Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -1,10 +1,12 @@ clean: rm -f httpserver.log rm -f parameters*.py rm -f -r applications/*/compiled find ./ -name '*~' -exec rm -f {} \; + find ./ -name '*.orig' -exec rm -f {} \; + find ./ -name '*.rej' -exec rm -f {} \; find ./ -name '#*' -exec rm -f {} \; find ./ -name 'Thumbs.db' -exec rm -f {} \; find ./gluon/ -name '.*' -exec rm -f {} \; find ./gluon/ -name '*class' -exec rm -f {} \; find ./applications/admin/ -name '.*' -exec rm -f {} \; @@ -20,12 +22,15 @@ rm -f -r applications/examples/static/epydoc/ epydoc --config epydoc.conf cp applications/examples/static/title.png applications/examples/static/epydoc tests: cd gluon/tests; ./test.sh 1>tests.log 2>&1 +update: + wget -O gluon/contrib/feedparser.py http://feedparser.googlecode.com/svn/trunk/feedparser/feedparser.py + wget -O gluon/contrib/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py src: - echo 'Version 1.98.2 ('`date +%Y-%m-%d\ %H:%M:%S`')' > VERSION + echo 'Version 1.99.2 ('`date +%Y-%m-%d\ %H:%M:%S`') stable' > VERSION ### rm -f all junk files make clean ### clean up baisc apps rm -f routes.py rm -f applications/*/sessions/* @@ -48,11 +53,11 @@ cp LICENSE applications/admin/ cp LICENSE applications/examples/ ### build web2py_src.zip echo '' > NEWINSTALL mv web2py_src.zip web2py_src_old.zip | echo 'no old' - cd ..; zip -r web2py/web2py_src.zip web2py/gluon/*.py web2py/gluon/contrib/* web2py/splashlogo.gif web2py/*.py web2py/ABOUT web2py/LICENSE web2py/README web2py/NEWINSTALL web2py/VERSION web2py/Makefile web2py/epydoc.css web2py/epydoc.conf web2py/app.example.yaml web2py/logging.example.conf web2py_exe.conf web2py/queue.example.yaml MANIFEST.in mkweb2pyenv startweb2py 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/gluon/*.py web2py/gluon/contrib/* web2py/splashlogo.gif web2py/*.py web2py/ABOUT web2py/LICENSE web2py/README web2py/NEWINSTALL web2py/VERSION web2py/Makefile web2py/epydoc.css web2py/epydoc.conf web2py/app.example.yaml web2py/logging.example.conf web2py_exe.conf web2py/queue.example.yaml MANIFEST.in w2p_apps w2p_clone w2p_run startweb2py 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: make epydoc make src make app @@ -102,13 +107,17 @@ cp -r applications/welcome ../web2py_win/web2py/applications cp -r applications/examples ../web2py_win/web2py/applications cp applications/__init__.py ../web2py_win/web2py/applications cd ../web2py_win; zip -r web2py_win.zip web2py mv ../web2py_win/web2py_win.zip . +pip: + # create Web2py distribution for upload to Pypi + # after upload clean Web2py sources with rm -R ./dist + python setup.py sdist run: python2.5 web2py.py -a hello push: make src echo '' > NEWINSTALL hg push bzr push bzr+ssh://mdipierro@bazaar.launchpad.net/~mdipierro/web2py/devel --use-existing-dir Index: README ================================================================== --- README +++ README @@ -1121,11 +1121,11 @@ - better description of --shell, thanks Anthony - extra SQLTABLE columns, thanks Martin - fixed toolbar conflics, thanks Simon - GAE password shows with **** -# 1.98.1 +# 1.98.1-1.98.2 - fixed some problems with LOAD(ajax=False), thanks Anthony - jquery 1.6.2 - gevent.pywsgi adds ssl support, thanks Vasile - import/export of blobs are base64 encoded - max number of login attemts in admin, thanks Ross @@ -1140,5 +1140,39 @@ - new CAT tag for no tags - request.user_agent(), thanks Ross - fixed fawps support - SQLFORM(...,separator=': ') now customizable - many small bug fixes + +## 1.99.1 +- gluon/contrib/simplejsonrpc.py +- gluon/contrib/redis_cache.py +- support for A(name,callback=url,target='id',delete='tr') +- support for A(name,component=url,target='id',delete='tr') +- new pip installer, thanks Chris Steel +- isapiwsgihandler.py +- dal expression.coalesce(*options) +- gluon/contrib/simplejsonrpc.py, thanks Mariano +- expire_sessions.py respects expiration time, thanks iceberg +- addressed this issue: http://fuelyourcoding.com/jquery-events-stop-misusing-return-false/ +- x509 support (thanks Michele) +- form.process() and for.validate() +- rocket upgrade (1.2.4) +- jQuery upgrade (1.6.3) +- new syntax rows[i]('tablename.fieldname') +- new query syntax field.contains(list,all=True or False) +- new SQLFORM.grid and SQLFORM.smartgrid (should replace crud.search and crud.select) +- support for natural language queries (english only) in SQLFORM.grid +- support for computed columns and additional links in SQLFORM.grid +- new style virtual fields (experimental): db.table.field=Field.Lazy(...) +- request.utcnow +- cleaner/simpler welcome/models/db.py and welcome layout.html +- response.include_meta() and response.include_files(), thanks Denes +- dal auto-reconnect on time-out connections +- COL and COLGROUP helpers +- addresed OWASP #10, thanks Anthony and Eric +- auth.settings.login_after_registration=True +- detection of mobile devices and @mobilize helper (view.mobile.html), thanks Angelo +- experimental gluon/scheduler.py +- scripts/make_min_web2py.py +- crud.search has more options, thanks Denes +- many bug fixes (thanks Jonathan, Michele, Fran and others) Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -Version 1.98.2 (2011-08-04 00:47:09) +Version 1.99.2 (2011-09-26 06:55:33) stable Index: __init__.py ================================================================== --- __init__.py +++ __init__.py @@ -1,1 +1,3 @@ + + Index: anyserver.py ================================================================== --- anyserver.py +++ anyserver.py @@ -7,11 +7,11 @@ License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) This file is based, althought a rewrite, on MIT code from the Bottle web framework. """ -import os, sys, optparse +import os, sys, optparse, urllib path = os.path.dirname(os.path.abspath(__file__)) os.chdir(path) sys.path = [path]+[p for p in sys.path if not p==path] import gluon.main from gluon.fileutils import read_file, write_file @@ -130,11 +130,21 @@ @staticmethod def eventlet(app,address, **options): from eventlet import wsgi, listen wsgi.server(listen(address), app) - + + @staticmethod + def mongrel2(app,address,**options): + import uuid + sys.path.append(os.path.abspath(os.path.dirname(__file__))) + from mongrel2 import handler + conn = handler.Connection(str(uuid.uuid4()), + "tcp://127.0.0.1:9997", + "tcp://127.0.0.1:9996") + mongrel2_handler(app,conn,debug=False) + def run(servername,ip,port,softcron=True,logging=False,profiler=None): if logging: application = gluon.main.appfactory(wsgiapp=gluon.main.wsgibase, logfilename='httpserver.log', @@ -144,10 +154,105 @@ if softcron: from gluon.settings import global_settings global_settings.web2py_crontype = 'soft' getattr(Servers,servername)(application,(ip,int(port))) +def mongrel2_handler(application,conn,debug=False): + """ + Based on : + https://github.com/berry/Mongrel2-WSGI-Handler/blob/master/wsgi-handler.py + + WSGI handler based on the Python wsgiref SimpleHandler. + A WSGI application should return a iterable op StringTypes. + Any encoding must be handled by the WSGI application itself. + """ + from wsgiref.handlers import SimpleHandler + try: + import cStringIO as StringIO + except: + import StringIO + + # TODO - this wsgi handler executes the application and renders a page + # in memory completely before returning it as a response to the client. + # Thus, it does not "stream" the result back to the client. It should be + # possible though. The SimpleHandler accepts file-like stream objects. So, + # it should be just a matter of connecting 0MQ requests/response streams to + # the SimpleHandler requests and response streams. However, the Python API + # for Mongrel2 doesn't seem to support file-like stream objects for requests + # and responses. Unless I have missed something. + + while True: + if debug: print "WAITING FOR REQUEST" + + # receive a request + req = conn.recv() + if debug: print "REQUEST BODY: %r\n" % req.body + + if req.is_disconnect(): + if debug: print "DISCONNECT" + continue #effectively ignore the disconnect from the client + + # Set a couple of environment attributes a.k.a. header attributes + # that are a must according to PEP 333 + environ = req.headers + environ['SERVER_PROTOCOL'] = 'HTTP/1.1' # SimpleHandler expects a server_protocol, lets assume it is HTTP 1.1 + environ['REQUEST_METHOD'] = environ['METHOD'] + if ':' in environ['Host']: + environ['SERVER_NAME'] = environ['Host'].split(':')[0] + environ['SERVER_PORT'] = environ['Host'].split(':')[1] + else: + environ['SERVER_NAME'] = environ['Host'] + environ['SERVER_PORT'] = '' + environ['SCRIPT_NAME'] = '' # empty for now + environ['PATH_INFO'] = urllib.unquote(environ['PATH']) + if '?' in environ['URI']: + environ['QUERY_STRING'] = environ['URI'].split('?')[1] + else: + environ['QUERY_STRING'] = '' + if environ.has_key('Content-Length'): + environ['CONTENT_LENGTH'] = environ['Content-Length'] # necessary for POST to work with Django + environ['wsgi.input'] = req.body + + if debug: print "ENVIRON: %r\n" % environ + + # SimpleHandler needs file-like stream objects for + # requests, errors and reponses + reqIO = StringIO.StringIO(req.body) + errIO = StringIO.StringIO() + respIO = StringIO.StringIO() + + # execute the application + handler = SimpleHandler(reqIO, respIO, errIO, environ, multithread = False, multiprocess = False) + handler.run(application) + + # Get the response and filter out the response (=data) itself, + # the response headers, + # the response status code and the response status description + response = respIO.getvalue() + response = response.split("\r\n") + data = response[-1] + headers = dict([r.split(": ") for r in response[1:-2]]) + code = response[0][9:12] + status = response[0][13:] + + # strip BOM's from response data + # Especially the WSGI handler from Django seems to generate them (2 actually, huh?) + # a BOM isn't really necessary and cause HTML parsing errors in Chrome and Safari + # See also: http://www.xs4all.nl/~mechiel/projects/bomstrip/ + # Although I still find this a ugly hack, it does work. + data = data.replace('\xef\xbb\xbf', '') + + # Get the generated errors + errors = errIO.getvalue() + + # return the response + if debug: print "RESPONSE: %r\n" % response + if errors: + if debug: print "ERRORS: %r" % errors + data = "%s\r\n\r\n%s" % (data, errors) + conn.reply_http(req, data, code = code, status = status, headers = headers) + def main(): usage = "python anyserver.py -s tornado -i 127.0.0.1 -p 8000 -l -P" try: version = read_file('VERSION') except IOError: @@ -158,11 +263,11 @@ action='store_true', default=False, dest='logging', help='log into httpserver.log') parser.add_option('-P', - '--profiler', + '--profiler', default=False, dest='profiler', help='profiler filename') servers = ', '.join(x for x in dir(Servers) if not x[0]=='_') parser.add_option('-s', @@ -186,8 +291,10 @@ dest='workers', help='number of workers number') (options, args) = parser.parse_args() print 'starting %s on %s:%s...' % (options.server,options.ip,options.port) run(options.server,options.ip,options.port,logging=options.logging,profiler=options.profiler) + if __name__=='__main__': main() + Index: appengine_config.py ================================================================== --- appengine_config.py +++ appengine_config.py @@ -1,4 +1,5 @@ def webapp_add_wsgi_middleware(app): from google.appengine.ext.appstats import recording app = recording.appstats_wsgi_middleware(app) return app + ADDED applications/admin/controllers/debug.py Index: applications/admin/controllers/debug.py ================================================================== --- applications/admin/controllers/debug.py +++ applications/admin/controllers/debug.py @@ -0,0 +1,33 @@ +import sys +import cStringIO +import gluon.contrib.shell +import code, thread +from gluon.debug import communicate + + +if DEMO_MODE or MULTI_USER_MODE: + session.flash = T('disabled in demo mode') + redirect(URL('default','site')) + +FE=10**9 + +def index(): + app = request.args(0) or 'admin' + reset() + # read buffer + data = communicate() + return dict(app=app,data=data) + +def callback(): + app = request.args[0] + command = request.vars.statement + session['debug_commands:'+app].append(command) + output = communicate(command) + k = len(session['debug_commands:'+app]) - 1 + return '[%i] %s%s\n' % (k + 1, command, output) + +def reset(): + app = request.args(0) or 'admin' + session['debug_commands:'+app] = [] + return 'done' + Index: applications/admin/controllers/default.py ================================================================== --- applications/admin/controllers/default.py +++ applications/admin/controllers/default.py @@ -586,12 +586,12 @@ return 'plus' if item[0] == '-': return 'minus' if request.vars: - c = ''.join([item[2:] for (i, item) in enumerate(d) if item[0] \ - == ' ' or 'line%i' % i in request.vars]) + c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0] \ + == ' ' or 'line%i' % i in request.vars]) safe_write(path, c) session.flash = 'files merged' redirect(URL('edit', args=request.args)) else: # Making the short circuit compatible with <= python2.4 @@ -708,12 +708,12 @@ data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) items = regex_expose.findall(data) functions[c] = items # Get all views - views = sorted(listdir(apath('%s/views/' % app, r=request), '[\w/\-]+\.\w+$')) - views = [x.replace('\\','/') for x in views] + views = sorted(listdir(apath('%s/views/' % app, r=request), '[\w/\-]+(\.\w+)+$')) + views = [x.replace('\\','/') for x in views if not x.endswith('.bak')] extend = {} include = {} for c in views: data = safe_read(apath('%s/views/%s' % (app, c), r=request)) items = regex_extend.findall(data) @@ -1190,11 +1190,11 @@ """ Update available languages """ app = get_app() update_all_languages(apath(app, r=request)) session.flash = T('Language files (static strings) updated') - redirect(URL('design',args=app)) + redirect(URL('design',args=app,anchor='languages')) def twitter(): session.forget() session._unlock(response) import gluon.tools @@ -1215,8 +1215,9 @@ return dict(form=auth()) else: return dict(form=T("Disabled")) def reload_routes(): - """ Reload routes.py """ - gluon.rewrite.load() - redirect(URL('site')) + """ Reload routes.py """ + import gluon.rewrite + gluon.rewrite.load() + redirect(URL('site')) Index: applications/admin/controllers/wizard.py ================================================================== --- applications/admin/controllers/wizard.py +++ applications/admin/controllers/wizard.py @@ -21,11 +21,12 @@ ('email_login',''), ('login_method','local'), ('login_config',''), ('plugins',[])], 'tables':['auth_user'], - 'table_auth_user':['username','first_name','last_name','email','password'], + 'table_auth_user':['username','first_name', + 'last_name','email','password'], 'pages':['index','error'], 'page_index':'# Welcome to my new app', 'page_error':'# Error: the document does not exist', } @@ -41,15 +42,17 @@ def index(): response.view='wizard/step.html' reset(session) apps=os.listdir(os.path.join(request.folder,'..')) - form=SQLFORM.factory(Field('name',requires=[IS_NOT_EMPTY(),IS_ALPHANUMERIC()])) + form=SQLFORM.factory(Field('name',requires=[IS_NOT_EMPTY(), + IS_ALPHANUMERIC()])) if form.accepts(request.vars): app = form.vars.name session.app['name'] = app - if MULTI_USER_MODE and db(db.app.name==app)(db.app.owner!=auth.user.id).count(): + if MULTI_USER_MODE and db(db.app.name==app)\ + (db.app.owner!=auth.user.id).count(): session.flash = 'App belongs already to other user' elif app in apps: meta = os.path.normpath(\ os.path.join(os.path.normpath(request.folder), '..',app,'wizard.metadata')) @@ -58,13 +61,13 @@ metafile = open(meta,'rb') try: session.app = pickle.load(metafile) finally: metafile.close() - session.flash = "The app exists, was created by wizard, continue to overwrite!" + session.flash = T("The app exists, was created by wizard, continue to overwrite!") except: - session.flash = "The app exists, was NOT created by wizard, continue to overwrite!" + session.flash = T("The app exists, was NOT created by wizard, continue to overwrite!") redirect(URL('step1')) return dict(step='Start',form=form) def step1(): @@ -118,24 +121,27 @@ def step2(): response.view='wizard/step.html' form=SQLFORM.factory(Field('table_names','list:string', default=session.app['tables'])) if form.accepts(request.vars): - table_names = [clean(t) for t in listify(form.vars.table_names) if t.strip()] - if [t for t in table_names if t.startswith('auth_') and not t=='auth_user']: - form.error.table_names = T('invalid table names (auth_* tables already defined)') + table_names = [clean(t) for t in listify(form.vars.table_names) \ + if t.strip()] + if [t for t in table_names if t.startswith('auth_') and \ + not t=='auth_user']: + form.error.table_names = \ + T('invalid table names (auth_* tables already defined)') else: session.app['tables']=table_names for table in session.app['tables']: if not 'table_'+table in session.app: session.app['table_'+table]=['name'] if not table=='auth_user': - for key in ['create','read','update','select','search']: - name = table+'_'+key - if not name in session.app['pages']: - session.app['pages'].append(name) - session.app['page_'+name]='## %s %s' % (key.capitalize(),table) + name = table+'_manage' + if not name in session.app['pages']: + session.app['pages'].append(name) + session.app['page_'+name] = \ + '## Manage %s\n{{=form}}' % (table) if session.app['tables']: redirect(URL('step3',args=0)) else: redirect(URL('step4')) return dict(step='2: Tables',form=form) @@ -164,11 +170,12 @@ else: if n=m: redirect(URL('step4')) page=session.app['pages'][n] markmin_url='http://web2py.com/examples/static/markmin.html' form=SQLFORM.factory(Field('content','text', default=session.app.get('page_'+page,[]), - comment=A('use markmin',_href=markmin_url,_target='_blank')), + comment=A('use markmin', + _href=markmin_url,_target='_blank')), formstyle='table2cols') if form.accepts(request.vars): session.app['page_'+page]=form.vars.content if n')}); jQuery('input.integer').live('keyup', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); jQuery('input.double,input.decimal').live('keyup', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();}); var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?"; jQuery("input[type='checkbox'].delete").live('click', function(){ if(this.checked) if(!confirm(confirm_message)) this.checked=false; }); @@ -53,14 +53,14 @@ }); function web2py_trap_form(action,target) { jQuery('#'+target+' form').each(function(i){ var form=jQuery(this); if(!form.hasClass('no_trap')) - form.submit(function(obj){ + form.submit(function(e){ jQuery('.flash').hide().html(''); web2py_ajax_page('post',action,form.serialize(),target); - return false; + e.preventDefault(); }); }); } function web2py_ajax_page(method,action,data,target) { jQuery.ajax({'type':method,'url':action,'data':data, Index: applications/admin/views/default/about.html ================================================================== --- applications/admin/views/default/about.html +++ applications/admin/views/default/about.html @@ -2,11 +2,11 @@ {{block sectionclass}}about{{end}}

{{=T("About application")}} "{{=app}}"

{{=T("About")}} {{=app}}

-

{{=button(URL('edit/%s/ABOUT' % (app)), T('edit'))}}

+

{{=button(URL('edit/%s/ABOUT' % (app)), T('Edit'))}}

{{=about}}

{{=T('License for')}} {{=app}}

-

{{=button(URL('edit/%s/LICENSE' % (app)), T('edit'))}}

+

{{=button(URL('edit/%s/LICENSE' % (app)), T('Edit'))}}

{{=license}}
Index: applications/admin/views/default/design.html ================================================================== --- applications/admin/views/default/design.html +++ applications/admin/views/default/design.html @@ -3,15 +3,15 @@ def all(items): return reduce(lambda a,b:a and b,items,True) def peekfile(path,file): return A(file.replace('\\\\','/'),_href=URL('peek', args=(app, path, file))) def editfile(path,file): - return A(SPAN(T('edit')),_class='button editbutton',_href=URL('edit', args=(app, path, file))) + return A(SPAN(T('Edit')),_class='button editbutton',_href=URL('edit', args=(app, path, file))) def testfile(path,file): return A(TAG[''](IMG(_src=URL('static', 'images/test_icon.png'), _alt=T('test')), SPAN(T("Run tests in this file (to run all files, you may also use the button labelled 'test')"))), _class='icon test tooltip',_href=URL('test', args=(app, file))) def editlanguagefile(path,file): - return A(SPAN(T('edit')),_class='button editbutton',_href=URL('edit_language', args=(app, path, file))) + return A(SPAN(T('Edit')),_class='button editbutton',_href=URL('edit_language', args=(app, path, file))) def file_upload_form(location): form=FORM(T("upload file:")," ", INPUT(_type="file",_name="file")," ",T("and rename it:")," ", INPUT(_type="text",_name="filename",requires=IS_NOT_EMPTY), INPUT(_type="hidden",_name="location",_value=location), @@ -21,11 +21,11 @@ def file_create_form(location): form=FORM(T("create file with filename:")," ", INPUT(_type="text",_name="filename",requires=IS_NOT_EMPTY), INPUT(_type="hidden",_name="location",_value=location), INPUT(_type="hidden",_name="sender",_value=URL('design',args=app)), - INPUT(_type="submit",_value=T("create")),_action=URL('create_file')) + INPUT(_type="submit",_value=T("Create")),_action=URL('create_file')) return form def upload_plugin_form(app): form=FORM(T("upload plugin file:")," ", INPUT(_type="file",_name="pluginfile"), INPUT(_type="submit",_value=T("upload"))) Index: applications/admin/views/default/peek.html ================================================================== --- applications/admin/views/default/peek.html +++ applications/admin/views/default/peek.html @@ -4,13 +4,13 @@

{{=T("Peeking at file")}} "{{=filename}}"

{{=button(URL('design',args=request.args[0]), T('back'))}} -{{=button(URL('edit',args=request.args), T('edit'))}} +{{=button(URL('edit',args=request.args), T('Edit'))}}

{{ if filename[-3:]=='.py': language='python' else: language='html' }} {{=CODE(data,language=language,link='/examples/global/vars/')}} Index: applications/admin/views/default/plugin.html ================================================================== --- applications/admin/views/default/plugin.html +++ applications/admin/views/default/plugin.html @@ -4,15 +4,15 @@ def all(items): return reduce(lambda a,b:a and b,items,True) def peekfile(path,file): return A(file.replace('\\\\','/'),_href=URL('peek', args=(app, path, file))) def editfile(path,file): - return A(SPAN(T('edit')),_class='button editbutton',_href=URL('edit', args=(app, path, file))) + return A(SPAN(T('Edit')),_class='button editbutton',_href=URL('edit', args=(app, path, file))) def testfile(path,file): return A(TAG[''](IMG(_src=URL('static', 'images/test_icon.png'), _alt=T('test')), SPAN(T("Run tests in this file"))), _class='icon test tooltip',_href=URL('test', args=(app, file))) def editlanguagefile(path,file): - return A(SPAN(T('edit')),_class='button editbutton',_href=URL('edit_language', args=(app, path, file))) + return A(SPAN(T('Edit')),_class='button editbutton',_href=URL('edit_language', args=(app, path, file))) def file_upload_form(location): form=FORM(T("upload file:")," ", INPUT(_type="file",_name="file")," ",T("and rename it:")," ", INPUT(_type="text",_name="filename",requires=IS_NOT_EMPTY), INPUT(_type="hidden",_name="location",_value=location), Index: applications/admin/views/default/site.html ================================================================== --- applications/admin/views/default/site.html +++ applications/admin/views/default/site.html @@ -15,29 +15,29 @@ {{else:}}

{{=A(a,_href=URL(a,'default','index'))}}

{{if MULTI_USER_MODE and db.app(name=a):}}(created by {{="%(first_name)s %(last_name)s" % db.auth_user[db.app(name=a).owner]}}){{pass}}

{{if not os.path.exists('applications/%s/compiled' % a):}} - {{=sp_button(URL('design',args=a), T("edit"))}} + {{=sp_button(URL('design',args=a), T("Edit"))}} {{else:}} {{=button(URL(a,'appadmin','index'), T("appadmin"))}} {{pass}} - {{=button(URL('about',args=a), T("about"))}} + {{=button(URL('about',args=a), T("About"))}} {{pass}} - {{=button(URL('errors',args=a), T("errors"))}} - {{=button(URL('cleanup',args=a), T("clean"))}} - {{=button(URL('pack',args=a), T("pack all"))}} + {{=button(URL('errors',args=a), T("Errors"))}} + {{=button(URL('cleanup',args=a), T("Clean"))}} + {{=button(URL('pack',args=a), T("Pack all"))}} {{if not os.path.exists('applications/%s/compiled' % a):}} - {{=button(URL('compile_app',args=a), T("compile"))}} + {{=button(URL('compile_app',args=a), T("Compile"))}} {{else:}} - {{=button(URL('pack',args=(a, 'compiled')), T("pack compiled"))}} + {{=button(URL('pack',args=(a, 'compiled')), T("Pack compiled"))}} {{if glob.glob('applications/%s/controllers/*.py' % a):}} - {{=button(URL('remove_compiled_app',args=a), T("remove compiled"))}} + {{=button(URL('remove_compiled_app',args=a), T("Remove compiled"))}} {{pass}} {{pass}} {{if a!=request.application:}} - {{=button(URL('uninstall',args=a), T("uninstall"))}} + {{=button(URL('uninstall',args=a), T("Uninstall"))}} {{pass}}

{{pass}} @@ -49,52 +49,53 @@
{{if MULTI_USER_MODE:}} {{=auth.navbar()}} {{else:}} - {{=sp_button(URL('change_password'), T('change admin password'))}} + {{=sp_button(URL('change_password'), T('Change admin password'))}} {{pass}}
{{if is_manager():}}
-

{{=myversion}}

+

{{="Version %s.%s.%s (%s) %s" % myversion}}

{{if session.check_version:}}

{{=T('Checking for upgrades...')}} {{session.check_version=False}} {{else:}}

- {{=button("javascript:ajax('"+URL('check_version')+"',[],'check_version')", T('check for upgrades'))}} + {{=button("javascript:ajax('"+URL('check_version')+"',[],'check_version')", T('Check for upgrades'))}} {{pass}}

Running on {{=request.env.server_software}}
+

{{=button(URL('default','reload_routes'), T('Reload routes'))}}

{{pass}}

{{=T("New application wizard")}}

-

{{=button(URL('wizard','index'), T('start wizard'))}} +

{{=button(URL('wizard','index'), T('Start wizard'))}} {{=T("(requires internet access)")}}

{{=T("New simple application")}}

{{=LABEL(T("Application name:"), _for="scaffold_filename")}} - +
-

{{=T("Upload & install packed application")}}

+

{{=T("Upload and install packed application")}}

@@ -113,37 +114,37 @@ OR
- {{=LABEL(T("Use an url:"), _for='upload_url')}} + {{=LABEL(T("Get from URL:"), _for='upload_url')}}
- {{=LABEL(T("overwrite installed app"), _for='upload_overwrite')}} + {{=LABEL(T("Overwrite installed app"), _for='upload_overwrite')}}
- +

{{=T("Deploy on Google App Engine")}}

-

{{=button(URL('gae','deploy'), T('deploy'))}}

+

{{=button(URL('gae','deploy'), T('Deploy'))}}


{{if TWITTER_HASH:}}

{{=T("%s Recent Tweets"%TWITTER_HASH)}}

{{=T('loading...')}}
Index: applications/admin/views/layout.html ================================================================== --- applications/admin/views/layout.html +++ applications/admin/views/layout.html @@ -26,11 +26,11 @@
{{=response.flash or ''}}
{{include}}
- {{pass}} -

{{=T("Import/Export")}}


- [ {{=T("export as csv file")}} ] - {{if table:}} - {{=FORM(str(T('or import from csv file'))+" ",INPUT(_type='file',_name='csvfile'),INPUT(_type='hidden',_value=table,_name='table'),INPUT(_type='submit',_value='import'))}} - {{pass}} - - -{{elif request.function=='insert':}} -

{{=T("database")}} {{=A(request.args[0],_href=URL('index'))}} - {{if hasattr(table,'_primarykey'):}} - {{fieldname=table._primarykey[0]}} - {{dbname=request.args[0]}} - {{tablename=request.args[1]}} - {{cond = table[fieldname].type in ['string','text'] and '!=""' or '>0'}} - {{=T("table")}} {{=A(tablename,_href=URL('select',args=dbname,vars=dict(query='%s.%s.%s%s'%(dbname,tablename,fieldname,cond))))}} - {{else:}} - {{=T("table")}} {{=A(request.args[1],_href=URL('select',args=request.args[0],vars=dict(query='%s.%s.id>0'%tuple(request.args[:2]))))}} - {{pass}} -

-

{{=T("New Record")}}


- {{=form}} - - - -{{elif request.function=='update':}} -

{{=T("database")}} {{=A(request.args[0],_href=URL('index'))}} - {{if hasattr(table,'_primarykey'):}} - {{fieldname=request.vars.keys()[0]}} - {{dbname=request.args[0]}} - {{tablename=request.args[1]}} - {{cond = table[fieldname].type in ['string','text'] and '!=""' or '>0'}} - {{=T("table")}} {{=A(tablename,_href=URL('select',args=dbname,vars=dict(query='%s.%s.%s%s'%(dbname,tablename,fieldname,cond))))}} - {{=T("record")}} {{=A('%s=%s'%request.vars.items()[0],_href=URL('update',args=request.args[:2],vars=request.vars))}} - {{else:}} - {{=T("table")}} {{=A(request.args[1],_href=URL('select',args=request.args[0],vars=dict(query='%s.%s.id>0'%tuple(request.args[:2]))))}} - {{=T("record id")}} {{=A(request.args[2],_href=URL('update',args=request.args[:3]))}} - {{pass}} -

-

{{=T("Edit current record")}}



{{=form}} - - - -{{elif request.function=='state':}} -

{{=T("Internal State")}}

-

{{=T("Current request")}}

- {{=BEAUTIFY(request)}} -

{{=T("Current response")}}

- {{=BEAUTIFY(response)}} -

{{=T("Current session")}}

- {{=BEAUTIFY(session)}} - - -{{elif request.function == 'ccache':}} -

Cache

-
-
-
- Statistics -
-
-

Overview

-

- Hit Ratio: - {{=total['ratio']}}% - ({{=total['hits']}} hits - and {{=total['misses']}} misses) -

-

- Size of cache: - {{=total['objects']}} items, - {{=total['bytes']}} bytes - {{if total['bytes'] > 524287:}} - ({{="%.0d" % (total['bytes'] / 1048576)}} MB) - {{pass}} -

-

- Cache contains items up to - {{="%02d" % total['oldest'][0]}} hours - {{="%02d" % total['oldest'][1]}} minutes - {{="%02d" % total['oldest'][2]}} seconds old. -

-

RAM

-

- Hit Ratio: - {{=ram['ratio']}}% - ({{=ram['hits']}} hits - and {{=ram['misses']}} misses) -

-

- Size of cache: - {{=ram['objects']}} items, - {{=ram['bytes']}} bytes - {{if ram['bytes'] > 524287:}} - ({{=ram['bytes'] / 1048576}} MB) - {{pass}} -

-

- RAM contains items up to - {{="%02d" % ram['oldest'][0]}} hours - {{="%02d" % ram['oldest'][1]}} minutes - {{="%02d" % ram['oldest'][2]}} seconds old. -

-

DISK

-

- Hit Ratio: - {{=disk['ratio']}}% - ({{=disk['hits']}} hits - and {{=disk['misses']}} misses) -

-

- Size of cache: - {{=disk['objects']}} items, - {{=disk['bytes']}} bytes - {{if disk['bytes'] > 524287:}} - ({{=disk['bytes'] / 1048576}} MB) - {{pass}} -

-

- DISK contains items up to - {{="%02d" % disk['oldest'][0]}} hours - {{="%02d" % disk['oldest'][1]}} minutes - {{="%02d" % disk['oldest'][2]}} seconds old. -

-
- -
- Manage Cache -
-
-

- {{=form}} -

-
-
-
-
-{{pass}} DELETED applications/examples/views/database_examples/buy.html Index: applications/examples/views/database_examples/buy.html ================================================================== --- applications/examples/views/database_examples/buy.html +++ applications/examples/views/database_examples/buy.html @@ -1,7 +0,0 @@ -{{extend 'layout_examples/layout_civilized.html'}} -

Purchase form

- {{=form}} - [ {{=A('reset purchased',_href=URL('reset_purchased'))}} | - {{=A('delete purchased',_href=URL('delete_purchased'))}} ]
-

Current purchases (SQL JOIN!)

-

{{=records}}

DELETED applications/examples/views/database_examples/register_dog.html Index: applications/examples/views/database_examples/register_dog.html ================================================================== --- applications/examples/views/database_examples/register_dog.html +++ applications/examples/views/database_examples/register_dog.html @@ -1,6 +0,0 @@ -{{extend 'layout_examples/layout_civilized.html'}} - -

Dog registration form

-{{=form}} -

Current dogs

-{{=records}} DELETED applications/examples/views/database_examples/register_product.html Index: applications/examples/views/database_examples/register_product.html ================================================================== --- applications/examples/views/database_examples/register_product.html +++ applications/examples/views/database_examples/register_product.html @@ -1,6 +0,0 @@ -{{extend 'layout_examples/layout_civilized.html'}} - -

Product registration form

-{{=form}} -

Current products

-{{=records}} DELETED applications/examples/views/database_examples/register_user.html Index: applications/examples/views/database_examples/register_user.html ================================================================== --- applications/examples/views/database_examples/register_user.html +++ applications/examples/views/database_examples/register_user.html @@ -1,6 +0,0 @@ -{{extend 'layout_examples/layout_civilized.html'}} - -

User registration form

-{{=form}} -

Current users

-{{=records}} DELETED applications/examples/views/default/changelog.html Index: applications/examples/views/default/changelog.html ================================================================== --- applications/examples/views/default/changelog.html +++ applications/examples/views/default/changelog.html @@ -1,5 +0,0 @@ -{{extend 'layout.html'}} - -
- {{=changelog}} -
DELETED applications/examples/views/default/documentation.html Index: applications/examples/views/default/documentation.html ================================================================== --- applications/examples/views/default/documentation.html +++ applications/examples/views/default/documentation.html @@ -1,18 +0,0 @@ -{{extend 'layout.html'}} - -
-
- {{=get_content('main')}} -
-
- {{=get_content('official')}} -
-
- {{=get_content('community')}} -
-
- {{=get_content('more')}} -
-
- - DELETED applications/examples/views/default/download.html Index: applications/examples/views/default/download.html ================================================================== --- applications/examples/views/default/download.html +++ applications/examples/views/default/download.html @@ -1,100 +0,0 @@ -{{response.files.append(URL('static','css/artwork.css'))}} -{{extend 'layout.html'}} -{{import os}} -{{version = request.env.web2py_version.split('(')}} -
-

web2pyTM Download

-
-
- - - - - - - - - - - -
Current (for everybody)
{{=version[0]}}
({{=version[1]}}
Nightly Built (for testers)Trunk (for developers)
- - - - - -
-
-

- The source code version works on all supported platforms, including Linux, but it requires Python 2.4, 2.5, 2.6, or 2.7. - It runs on Windows and most Unix systems, including Linux and BSD. -

-
- -

Instructions

-

After download, unzip it and click on web2py.exe (windows) or web2py.app (osx). - To run from source type:

- {{=CODE("python2.5 web2py.py",language=None,counter='>',_class='boxCode')}} -

or for more info type:

- {{=CODE("python2.5 web2py.py -h",language=None,counter='>',_class='boxCode')}} - - -

Caveats

-

After installation, every time you run it, web2py asks you to choose a password. This password is your administrative password. If the password is left blank, the administrative interface is disabled. The administrative interface /admin/default/index is only accessible via localhost and always requires a password.
Any url /a/b/c maps into a call to application a, controller b.py and function c in that controller.
You are strongly advised to also use Apache with mod_proxy or mod_wsgi to access applications in the framework. This allows better security and concurrency.

- -

License

-

Web2py code is released under LGPLv3 License. This license does not extend to third party libraries distributed with web2py (which can be MIT, BSD or Apache type licenses) nor does it extend to applications built with web2py (under the terms of the LGPL.

-

Applications built with web2py can be released under any license the author wishes as long they do not contain web2py code. They can link unmodified web2py libraries and they can be distributed with official web2py binaries. In particular web2py applications can be distributed in closed source. The admin interface provides a button to byte-code compile.

-

It is fine to distribute web2py (source or compiled) with your applications as long as you make it clear in the license where your application ends and web2py starts.

-

web2py is copyrighted by Massimo Di Pierro. The web2py trademark is owned by Massimo Di Pierro.

- [read more] - -

Applications

-

You can find many free and ready to use web2py applications with source code here. -

-
-
- web2py artwork - - - -
-
- Stickers - - - - - - - -
- Download WEB2PY artwork pack in editable .png format -
- -
- - Logo, Stickers and Layout developed by José V. Sousa and Bruno Rocha (at Blouweb) All rights reserved by Massimo Di Pierro © 2010 -
- Favicon and HTML5 compatibility by Martin Mulone -
- Icon set made by Christian Burprich licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License -
-
-{{block sidebar}}{{end}} -{{block leftbadges}}{{end}} DELETED applications/examples/views/default/examples.html Index: applications/examples/views/default/examples.html ================================================================== --- applications/examples/views/default/examples.html +++ applications/examples/views/default/examples.html @@ -1,685 +0,0 @@ -{{extend 'layout.html'}} -{{import os}} - -
-

web2pyTM Examples

- -
- -

Simple Examples

- -

Here are some working and complete examples that explain the basic syntax of the framework.
- You can click on the web2py keywords (in the highlighted code!) to get documentation.

- -

Example {{c=1}}{{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def hello1(): - return "Hello World" - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} - -

If the controller function returns a string, that is the body of the rendered page.
Try it here: hello1

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def hello2(): - return T("Hello World") - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} - -

The function T() marks strings that need to be translated. Translation dictionaries can be created at /admin/default/design
Try it here: hello2

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def hello3(): - return dict(message=T("Hello World")) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} - - and view: simple_examples/hello3.html - {{=CODE(open(os.path.join(request.folder,'views/simple_examples/hello3.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

If you return a dictionary, the variables defined in the dictionery are visible to the view (template). -
Try it here: hello3

- -

Actions can also be be rendered in other formsts like JSON, hello3.json, and XML, hello3.xml

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def hello4(): - response.view='simple_examples/hello3.html' - return dict(message=T("Hello World")) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can change the view, but the default is /[controller]/[function].html. If the default is not found web2py tries to render the page using the generic.html view. -
Try it here: hello4

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def hello5(): - return HTML(BODY(H1(T('Hello World'),_style="color: red;"))).xml() # .xml to serialize - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can also generate HTML using helper objects HTML, BODY, H1, etc. Each of these tags is a class and the views know how to render the corresponding objects. The method .xml() serializes them and produce html/xml code for the page. - Each tag, DIV for example, takes three types of arguments:

-
    -
  • unnamed arguments, they correspond to nested tags
  • -
  • named arguments and name starts with '_'. These are mapped blindly into tag attributes and the '_' is removed. attributes without value like "READONLY" can be created with the argument "_readonly=ON".
  • -
  • named arguments and name does not start with '_'. They have a special meaning. See "value=" for INPUT, TEXTAREA, SELECT tags later. -
-

Try it here: hello5

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def hello6(): - response.flash=T("Hello World in a flash!") - return dict(message=T("Hello World")) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} - -

response.flash allows you to flash a message to the user when the page is returned. Use session.flash instead of response.flash to display a message after redirection. With default layout, you can click on the flash to make it disappear. -
Try it here: hello6

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def status(): - return dict(request=request,session=session,response=response) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Here we are showing the request, session and response objects using the generic.html template. -
Try it here: status

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def redirectme(): - redirect(URL('hello3')) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can do redirect. -
Try it here: redirectme

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def raisehttp(): - raise HTTP(400,"internal error") - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can raise HTTP exceptions to return an error page. -
Try it here: raisehttp

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def raiseexception(): - 1/0 - return 'oops' - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

If an exception occurs (other than HTTP) a ticket is generated and the event is logged for the administrator. These tickets and logs can be accessed, reviewed and deleted at any later time. -
Try it here: raiseexception

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def servejs(): - import gluon.contenttype - response.headers['Content-Type']=gluon.contenttype.contenttype('.js') - return 'alert("This is a Javascript document, it is not supposed to run!");' - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can serve other than HTML pages by changing the contenttype via the response.headers. The gluon.contenttype module can help you figure the type of the file to be served. NOTICE: this is not necessary for static files unless you want to require authorization. -
Try it here: servejs

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def makejson(): - return response.json(['foo', {'bar': ('baz', None, 1.0, 2)}]) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

If you are into Ajax, web2py includes gluon.contrib.simplejson, developed by Bob Ippolito. This module provides a fast and easy way to serve asynchronous content to your Ajax page. gluon.simplesjson.dumps(...) can serialize most Python types into JSON. gluon.contrib.simplejson.loads(...) performs the reverse operation. -
Try it here: makejson

- -

New in web2py 1.63: Any normal action returning a dict is automatically serialized in JSON if '.json' is appended to the URL.

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def makertf(): - import gluon.contrib.pyrtf as q - doc=q.Document() - section=q.Section() - doc.Sections.append(section) - section.append('Section Title') - section.append('web2py is great. '*100) - response.headers['Content-Type']='text/rtf' - return q.dumps(doc) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

web2py also includes gluon.contrib.pyrtf, developed by Simon Cusack and revised by Grant Edwards. This module allows you to generate Rich Text Format documents including colored formatted text and pictures.
Try it here: makertf

- -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def rss_aggregator(): - import datetime - import gluon.contrib.rss2 as rss2 - import gluon.contrib.feedparser as feedparser - d = feedparser.parse("http://rss.slashdot.org/Slashdot/slashdot/to") - - rss = rss2.RSS2(title=d.channel.title, - link = d.channel.link, - description = d.channel.description, - lastBuildDate = datetime.datetime.now(), - items = [ - rss2.RSSItem( - title = entry.title, - link = entry.link, - description = entry.description, - # guid = rss2.Guid('unkown'), - pubDate = datetime.datetime.now()) for entry in d.entries] - ) - response.headers['Content-Type']='application/rss+xml' - return rss2.dumps(rss) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

web2py includes gluon.contrib.rss2, developed by Dalke Scientific Software, which generates RSS2 feeds, and - gluon.contrib.feedparser, developed by Mark Pilgrim, which collects RSS and ATOM feeds. The above controller collects a slashdot feed and makes new one. -
Try it here: rss_aggregator

- - -

Example {{=c}}{{c+=1}}

In controller: simple_examples.py - {{=CODE(""" - def ajaxwiki(): - form=FORM(TEXTAREA(_id='text',_name='text'), - INPUT(_type='button',_value='markmin', - _onclick="ajax('ajaxwiki_onclick',['text'],'html')")) - return dict(form=form,html=DIV(_id='html')) - - def ajaxwiki_onclick(): - return MARKMIN(request.vars.text).xml() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

The markmin wiki markup is described here. - web2py also includes gluon.contrib.markdown.WIKI helper (markdown2) which converts WIKI markup to HTML following this syntax. In this example we added a fancy ajax effect.
Try it here: ajaxwiki

- -

Session Examples

- - -

Example {{=c}}{{c+=1}}

In controller: session_examples.py - {{=CODE(""" - def counter(): - if not session.counter: session.counter=0 - session.counter+=1 - return dict(counter=session.counter) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: session_examples/counter.html - {{=CODE(open(os.path.join(request.folder,'views/session_examples/counter.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Click to count. The session.counter is persistent for this user and application. Every applicaiton within the system has its own separate session management. -
Try it here: counter

- -

Template Examples

- - -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def variables(): return dict(a=10, b=20) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/variables.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/variables.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

A view (also known as template) is just an HTML file with {{...}} tags. You can put ANY python code into the tags, no need to indent but you must use pass to close blocks. The view is transformed into a python code and then executed. {{=a}} prints a.xml() or escape(str(a)). -
Try it here: variables

- -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def test_for(): return dict() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/test_for.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/test_for.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can do for and while loops. -
Try it here: test_for

- -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def test_if(): return dict() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/test_if.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/test_if.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can do if, elif, else. -
Try it here: test_if

- -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def test_try(): return dict() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/test_try.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/test_try.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can do try, except, finally. -
Try it here: test_try

- -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def test_def(): return dict() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/test_def.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/test_def.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can write functions in HTML too. -
Try it here: test_def

- -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def escape(): return dict(message='

text is escaped

') - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/escape.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/escape.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

The argument of {{=...}} is always escaped unless it is an object with a .xml() method such as link, A(...), a FORM(...), a XML(...) block, etc. -
Try it here: escape

- -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def xml(): - return dict(message=XML('

text is not escaped

')) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/xml.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/xml.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

If you do not want to escape the argument of {{=...}} mark it as XML. -
Try it here: xml

- -

Example {{=c}}{{c+=1}}

In controller: template_examples.py - {{=CODE(""" - def beautify(): return dict(message=BEAUTIFY(request)) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: template_examples/beautify.html - {{=CODE(open(os.path.join(request.folder,'views/template_examples/beautify.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can use BEAUTIFY to turn lists and dictionaries into organized HTML. -
Try it here: beautify

- -

Layout Examples

- - -

Example {{=c}}{{c+=1}}

In controller: layout_examples.py - {{=CODE(""" - def civilized(): - response.menu=[['civilized',True,URL('civilized')], - ['slick',False,URL('slick')], - ['basic',False,URL('basic')]] - response.flash='you clicked on civilized' - return dict(message="you clicked on civilized") - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: layout_examples/civilized.html - {{=CODE(open(os.path.join(request.folder,'views/layout_examples/civilized.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can specify the layout file at the top of your view. civilized Layout file is a view that somewhere in the body contains {{include}}. -
Try it here: civilized

- -

Example {{=c}}{{c+=1}}

In controller: layout_examples.py - {{=CODE(""" - def slick(): - response.menu=[['civilized',False,URL('civilized')], - ['slick',True,URL('slick')], - ['basic',False,URL('basic')]] - response.flash='you clicked on slick' - return dict(message="you clicked on slick") - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: layout_examples/slick.html - {{=CODE(open(os.path.join(request.folder,'views/layout_examples/slick.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Same here, but using a different template.
Try it here: slick

- -

Example {{=c}}{{c+=1}}

In controller: layout_examples.py - {{=CODE(""" - def basic(): - response.menu=[['civilized',False,URL('civilized')], - ['slick',False,URL('slick')], - ['basic',True,URL('basic')]] - response.flash='you clicked on basic' - return dict(message="you clicked on basic") - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: layout_examples/basic.html - {{=CODE(open(os.path.join(request.folder,'views/layout_examples/basic.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

'layout.html' is the default template, every application has a copy of it. -
Try it here: basic

- -

Form Examples

- - -

Example {{=c}}{{c+=1}}

In controller: form_examples.py - {{=CODE(""" - def form(): - form=FORM(TABLE(TR("Your name:",INPUT(_type="text",_name="name",requires=IS_NOT_EMPTY())), - TR("Your email:",INPUT(_type="text",_name="email",requires=IS_EMAIL())), - TR("Admin",INPUT(_type="checkbox",_name="admin")), - TR("Sure?",SELECT('yes','no',_name="sure",requires=IS_IN_SET(['yes','no']))), - TR("Profile",TEXTAREA(_name="profile",value="write something here")), - TR("",INPUT(_type="submit",_value="SUBMIT")))) - if form.accepts(request,session): - response.flash="form accepted" - elif form.errors: - response.flash="form is invalid" - else: - response.flash="please fill the form" - return dict(form=form,vars=form.vars) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

You can use HTML helpers like FORM, INPUT, TEXTAREA, OPTION, SELECT to build forms. The "value=" attribute sets the initial value of the field (works for TEXTAREA and OPTION/SELECT too) and the requires attribute sets the validators. - FORM.accepts(..) tries to validate the form and, on success, stores vars into form.vars. On failure the error messages are stored into form.errors and shown in the form. -
Try it here: form

- -

Database Examples

- -

You can find more examples of the web2py Database Abstraction Layer here

- -

Let's create a simple model with users, dogs, products and purchases (the database of an animal store). Users can have many dogs (ONE TO MANY), can buy many products and every product can have many buyers (MANY TO MANY).

- -

Example {{=c}}{{c+=1}}

in model: db.py - {{=CODE(""" - db=DAL('sqlite://storage.db') - - db.define_table('users', - Field('name'), - Field('email')) - - # ONE (users) TO MANY (dogs) - db.define_table('dogs', - Field('owner_id',db.users), - Field('name'), - Field('type'), - Field('vaccinated','boolean',default=False), - Field('picture','upload',default='')) - - db.define_table('products', - Field('name'), - Field('description','text')) - - # MANY (users) TO MANY (products) - db.define_table('purchases', - Field('buyer_id',db.users), - Field('product_id',db.products), - Field('quantity','integer')) - - purchased=((db.users.id==db.purchases.buyer_id)&(db.products.id==db.purchases.product_id)) - - db.users.name.requires=IS_NOT_EMPTY() - db.users.email.requires=[IS_EMAIL(), IS_NOT_IN_DB(db,'users.email')] - db.dogs.owner_id.requires=IS_IN_DB(db,'users.id','users.name') - db.dogs.name.requires=IS_NOT_EMPTY() - db.dogs.type.requires=IS_IN_SET(['small','medium','large']) - db.purchases.buyer_id.requires=IS_IN_DB(db,'users.id','users.name') - db.purchases.product_id.requires=IS_IN_DB(db,'products.id','products.name') - db.purchases.quantity.requires=IS_INT_IN_RANGE(0,10) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

- Tables are created if they do not exist (try... except). - Here "purchased" is an SQLQuery object, "db(purchased)" would be a SQLSet objects. A SQLSet object can be selected, updated, deleted. SQLSets can also be intersected. Allowed field types are string, integer, password, text, blob, upload, date, time, datetime, references(*), and id(*). The id field is there by default and must not be declared. references are for one to many and many to many as in the example above. For strings you should specify a length or you get length=32.

- You can use db.tablename.fieldname.requires= to set restrictions on the field values. These restrictions are automatically converted into widgets when generating forms from the table with SQLFORM(db.tablename). -

- define_tables creates the table and attempts a migration if table has changed or if database name has changed since last time. If you know you already have the table in the database and you do not want to attempt a migration add one last argument to define_table migrate=False.

- - -

Example {{=c}}{{c+=1}}

In controller: database_examples.py - {{=CODE(""" - response.menu=[['Register User',False,URL('register_user')], - ['Register Dog',False,URL('register_dog')], - ['Register Product',False,URL('register_product')], - ['Buy product',False,URL('buy')]] - - def register_user(): - ### create an insert form from the table - form=SQLFORM(db.users) - ### if form is correct, perform the insert - if form.accepts(request,session): - response.flash='new record inserted' - ### and get a list of all users - records=SQLTABLE(db().select(db.users.ALL)) - return dict(form=form,records=records) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: database_examples/register_user.html - {{=CODE(open(os.path.join(request.folder,'views/database_examples/register_user.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

This is a simple user registration form. SQLFORM takes a table and returns the corresponding entry form with validators, etc. SQLFORM.accepts is similar to FORM.accepts but, if form is validated, the corresponding insert is also performed. SQLFORM can also do update and edit if a record is passed as its second argument. - SQLTABLE instead turns a set of records (result of a select) into an HTML table with links as specified by its optional parameters. - The response.menu on top is just a variable used by the layout to make the navigation menu for all functions in this controller.
- Try it here: register_user

- -

Example {{=c}}{{c+=1}}

In controller: database_examples.py - {{=CODE(""" - def register_dog(): - form=SQLFORM(db.dogs) - if form.accepts(request,session): - response.flash='new record inserted' - download=URL('download') # to see the picture - records=SQLTABLE(db().select(db.dogs.ALL),upload=download) - return dict(form=form,records=records) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: database_examples/register_dog.html - {{=CODE(open(os.path.join(request.folder,'views/database_examples/register_dog.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Here is a dog registration form. Notice that the "image" (type "upload") field is rendered into a <INPUT type="file"> html tag. SQLFORM.accepts(...) handles the upload of the file into the uploads/ folder. -
Try it here: register_dog

- -

Example {{=c}}{{c+=1}}

In controller: database_examples.py - {{=CODE(""" - def register_product(): - form=SQLFORM(db.products) - if form.accepts(request,session): - response.flash='new record inserted' - records=SQLTABLE(db().select(db.products.ALL)) - return dict(form=form,records=records) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: database_examples/register_product.html - {{=CODE(open(os.path.join(request.folder,'views/database_examples/register_product.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Nothing new here. -
Try it here: register_product

- -

Example {{=c}}{{c+=1}}

In controller: database_examples.py - {{=CODE(""" - def buy(): - form=FORM(TABLE(TR("Buyer id:",INPUT(_type="text",_name="buyer_id",requires=IS_NOT_EMPTY())), - TR("Product id:",INPUT(_type="text",_name="product_id",requires=IS_NOT_EMPTY())), - TR("Quantity:",INPUT(_type="text",_name="quantity",requires=IS_INT_IN_RANGE(1,100))), - TR("",INPUT(_type="submit",_value="Order")))) - if form.accepts(request,session): - ### check if user is in the database - if len(db(db.users.id==form.vars.buyer_id).select())==0: - form.errors.buyer_id="buyer not in database" - ### check if product is in the database - if len(db(db.products.id==form.vars.product_id).select())==0: - form.errors.product_id="product not in database" - ### if no errors - if len(form.errors)==0: - ### get a list of same purchases by same user - purchases=db((db.purchases.buyer_id==form.vars.buyer_id)& - (db.purchases.product_id==form.vars.product_id)).select() - ### if list contains a record, update that record - if len(purchases)>0: - purchases[0].update_record(quantity=purchases[0].quantity+form.vars.quantity) - ### or insert a new record in table - else: - db.purchases.insert(buyer_id=form.vars.buyer_id, - product_id=form.vars.product_id, - quantity=form.vars.quantity) - response.flash="product purchased!" - if len(form.errors): response.flash="invalid valus in form!" - ### now get a list of all purchases - records=db(purchased).select(db.users.name,db.purchases.quantity,db.products.name) - return dict(form=form,records=SQLTABLE(records),vars=form.vars,vars2=request.vars) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}and view: database_examples/buy.html - {{=CODE(open(os.path.join(request.folder,'views/database_examples/buy.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Here is a rather sophisticated buy form. It checks that the buyer and the product are in the database and updates the corresponding record or inserts a new purchase. It also does a JOIN to list all purchases. -
Try it here: buy

- -

Example {{=c}}{{c+=1}}

In controller: database_examples.py - {{=CODE(""" - def delete_purchased(): - db(db.purchases.id>0).delete() - redirect(URL('buy')) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}}Try it here: delete_purchased - -

Example {{=c}}{{c+=1}}

In controller: database_examples.py - {{=CODE(""" - def reset_purchased(): - db(db.purchases.id>0).update(quantity=0) - redirect(URL('buy')) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

This is an update on an SQLSet. (db.purchase.id>0 identifies the set containing only table db.purchases.) -
Try it here: reset_purchased

- -

Example {{=c}}{{c+=1}}

In controller: database_examples.py - {{=CODE(""" - def download(): - return response.download(request,db) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

This controller allows users to download the uploaded pictures of the dogs. - Remember the upload=URL(...'download'...) statement in the register_dog function. Notice that in the URL path /application/controller/function/a/b/etc a, b, etc are passed to the controller as request.args[0], request.args[1], etc. Since the URL is validated request.args[] always contain valid filenames and no '~' or '..' etc. This is useful to allow visitors to link uploaded files.

- -

Cache Examples

- -

Example {{=c}}{{c+=1}}

In controller: cache_examples.py - {{=CODE(""" - def cache_in_ram(): - import time - t=cache.ram('time',lambda:time.ctime(),time_expire=5) - return dict(time=t,link=A('click to reload',_href=URL(r=request))) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

The output of lambda:time.ctime() is cached in ram for 5 seconds. The string 'time' is used as cache key. -
Try it here: cache_in_ram

- - -

Example {{=c}}{{c+=1}}

In controller: cache_examples.py - {{=CODE(""" - def cache_on_disk(): - import time - t=cache.disk('time',lambda:time.ctime(),time_expire=5) - return dict(time=t,link=A('click to reload',_href=URL(r=request))) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

The output of lambda:time.ctime() is cached on disk (using the shelve module) for 5 seconds. -
Try it here: cache_on_disk

- -

Example {{=c}}{{c+=1}}

In controller: cache_examples.py - {{=CODE(""" - def cache_in_ram_and_disk(): - import time - t=cache.ram('time',lambda:cache.disk('time', - lambda:time.ctime(),time_expire=5),time_expire=5) - return dict(time=t,link=A('click to reload',_href=URL(r=request))) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

The output of lambda:time.ctime() is cached on disk (using the shelve module) and then in ram for 5 seconds. web2py looks in ram first and if not there it looks on disk. If it is not on disk it calls the function. This is useful in a multiprocess type of environment. The two times do not have to be the same. -
Try it here: cache_in_ram_and_disk

- - -

Example {{=c}}{{c+=1}}

In controller: cache_examples.py - {{=CODE(""" - @cache(request.env.path_info,time_expire=5,cache_model=cache.ram) - def cache_controller_in_ram(): - import time - t=time.ctime() - return dict(time=t,link=A('click to reload',_href=URL(r=request)))""".strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Here the entire controller (dictionary) is cached in ram for 5 seconds. The result of a select cannot be cached unless it is first serialized into a table lambda:SQLTABLE(db().select(db.users.ALL)).xml(). You can read below for an even better way to do it. -
Try it here: cache_controller_in_ram

- -

Example {{=c}}{{c+=1}}

In controller: cache_examples.py - {{=CODE(""" - @cache(request.env.path_info,time_expire=5,cache_model=cache.disk) - def cache_controller_on_disk(): - import time - t=time.ctime() - return dict(time=t,link=A('click to reload',_href=URL(r=request))) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Here the entire controller (dictionary) is cached on disk for 5 seconds. This will not work if the dictionary contains unpickleable objects. -
Try it here: cache_controller_on_disk

- -

Example {{=c}}{{c+=1}}

In controller: cache_examples.py - {{=CODE(""" - @cache(request.env.path_info,time_expire=5,cache_model=cache.ram) - def cache_controller_and_view(): - import time - t=time.ctime() - d=dict(time=t,link=A('click to reload',_href=URL(r=request))) - return response.render(d) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

response.render(d) renders the dictionary inside the controller, so everything is cached now for 5 seconds. This is best and fastest way of caching! -
Try it here: cache_controller_and_view

- -

Example {{=c}}{{c+=1}}

In controller: cache_examples.py - {{=CODE(""" - def cache_db_select(): - import time - db.users.insert(name='somebody',email='gluon@mdp.cti.depaul.edu') - records=db().select(db.users.ALL,cache=(cache.ram,5)) - if len(records)>20: db(db.users.id>0).delete() - return dict(records=records) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

The results of a select are complex unpickleable objects that cannot be cached using the previous method, but the select command takes an argument cache=(cache_model,time_expire) and will cache the result of the query accordingly. Notice that the key is not necessary since key is generated based on the database name and the select string. -
Try it here: cache_db_select

- -

Ajax Examples

- - -

Example {{=c}}{{c+=1}}

In controller: ajax_examples.py - {{=CODE(""" - def index(): - return dict() - - def data(): - if not session.m or len(session.m)==10: session.m=[] - if request.vars.q: session.m.append(request.vars.q) - session.m.sort() - return TABLE(*[TR(v) for v in session.m]).xml() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} - In view: ajax_examples/index.html - {{=CODE(open(os.path.join(request.folder,'views/ajax_examples/index.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

The javascript function "ajax" is provided in "web2py_ajax.html" and included by "layout.html". It takes three arguments, a url, a list of ids and a target id. When called, it sends to the url (via a get) the values of the ids and display the response in the value (of innerHTML) of the target id. -
Try it here: index

- -

Example {{=c}}{{c+=1}}

In controller: ajax_examples.py - {{=CODE(""" - def flash(): - response.flash='this text should appear!' - return dict() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Try it here: flash

- -

Example {{=c}}{{c+=1}}

In controller: ajax_examples.py - {{=CODE(""" - def fade(): - return dict() - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} - In view: ajax_examples/fade.html
- {{=CODE(open(os.path.join(request.folder,'views/ajax_examples/fade.html'),'r').read(),language='html',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

Try it here: fade

- -

Excel-like spreadsheet via Ajax

- Web2py includes a widget that acts like an Excel-like spreadsheet and can be used to build forms - [read more]. - -

Testing Examples

- - -

Example {{=c}}{{c+=1}}

-

Using the Python doctest notation it is possible to write tests for all controller functions. Tests are then run via the administrative interface which generates a report. Here is an example of a test in the code: - {{=CODE(""" - def index(): - ''' - This is a docstring. The following 3 lines are a doctest: - >>> request.vars.name='Max' - >>> index() - {'name': 'Max'} - ''' - return dict(name=request.vars.name) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

- -

Streaming Examples

- - -

Example {{=c}}{{c+=1}}

-

It is very easy in web2py to stream large files. Here is an example of a controller that does so:

- {{=CODE(""" - def streamer(): - import os - path=os.path.join(request.folder,'private','largefile.mpeg4') - return response.stream(open(path,'rb'),chunk_size=4096) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} -

By default all static files and files stored in 'upload' fields in the database are streamed when larger than 1MByte.

- -

web2py automatically and transparently handles PARTIAL_CONTENT and RANGE requests.

- -

XML-RPC Examples

- -

Example {{=c}}{{c+=1}}

-

Web2py has native support for the XMLRPC protocol. Below is a controller function "handler" that exposes two functions, "add" and "sub" via XMLRPC. The controller "tester" executes the two function remotely via xmlrpc.

- {{=CODE(""" - from gluon.tools import Service - service = Service(globals()) - - @service.xmlrpc - def add(a,b): return a+b - - @service.xmlrpc - def sub(a,b): return a-b - - def call(): return service() - - def tester(): - import xmlrpclib - server=xmlrpclib.ServerProxy('http://hostname:port/app/controller/call/xmlrpc') - return str(server.add(3,4)+server.sub(3,4)) - """.strip(),language='web2py',link=URL(r=request,c='global',f='vars'),_class='boxCode')}} - -
-
-{{block sidebar}}{{end}} DELETED applications/examples/views/default/index.html Index: applications/examples/views/default/index.html ================================================================== --- applications/examples/views/default/index.html +++ applications/examples/views/default/index.html @@ -1,32 +0,0 @@ -{{extend 'layout.html'}} - -
-
- {{=get_content('maincontent')}} -
-
- {{=get_content('whyweb2py')}} -
-
-

{{=T('Recent development')}}

- {{=code_feed_reader(project='web2py',mode='nodiv')}} -
-
-

{{=T('Latest posts from user forum')}}

- {{=group_feed_reader(group='web2py',mode='nodiv')}} -
-
- -{{block sidebar}} - {{super}} - -
-
- -
-
-
-
-{{end}} DELETED applications/examples/views/default/license.html Index: applications/examples/views/default/license.html ================================================================== --- applications/examples/views/default/license.html +++ applications/examples/views/default/license.html @@ -1,7 +0,0 @@ -{{extend 'layout.html'}} - -
-

web2py License Agreement

- {{=license}} -
- DELETED applications/examples/views/default/support.html Index: applications/examples/views/default/support.html ================================================================== --- applications/examples/views/default/support.html +++ applications/examples/views/default/support.html @@ -1,40 +0,0 @@ -{{extend 'layout.html'}} -
-

Support for web2pyTM

- -

You can get a lot of free support by joining our mailing list. We also have a FAQ page.

- -

Affiliated Companies

- -

For long term professional support, code review and contract work you can contact our core developers:

- -
- -
- -

For professional support you can also contact one of the companies below:

- -
- - -
-
- -{{block leftbadges}}{{end}} DELETED applications/examples/views/default/usergroups.html Index: applications/examples/views/default/usergroups.html ================================================================== --- applications/examples/views/default/usergroups.html +++ applications/examples/views/default/usergroups.html @@ -1,9 +0,0 @@ -{{extend 'layout.html'}} - -
-
- {{=get_content('grouplist')}} -
-
- - DELETED applications/examples/views/default/videos.html Index: applications/examples/views/default/videos.html ================================================================== --- applications/examples/views/default/videos.html +++ applications/examples/views/default/videos.html @@ -1,16 +0,0 @@ -{{extend 'layout.html'}} -

-
-

{{=T('web2py videos')}}

-
- - - - - - - -
-
- - DELETED applications/examples/views/default/what.html Index: applications/examples/views/default/what.html ================================================================== --- applications/examples/views/default/what.html +++ applications/examples/views/default/what.html @@ -1,58 +0,0 @@ -{{extend 'layout.html'}} {{import os}} -
-

What is web2pyTM

-
-

The best way to understand web2py is to try it. - You can try it online here.
- This online version is identical to the actual web2py although some functions are disabled for security reasons.

-
-

web2py was inspired by Ruby on Rails and, as Rails, it focuses on rapid development and follows a Model View Controller design. web2py differs from Rails because it is based on Python (thus it is faster and more scalable), because it provides a comprehensive web-based administrative interface (thus there is no need to ever type shell commands unless you wish), includes libraries to handle more protocols (for example XML-RPC and RSS feeds), and can run on the Google App Engine.

- -

web2py was also inspired by Django and, as Django, it has the ability to generate forms from database tables and it includes an extensive set of validators. web2py differs from Django because it is more compact, easier to learn and does not have any project-level configuration files.

- -

web2py is less verbose than Java-based frameworks and its syntax is much cleaner than PHP-based frameworks. This makes applications simpler to develop, and easier to read - and maintain.

-
-

Here is a features comparison of web2py vs other popular web frameworks -

-

web2py comes in source code version (for any Operating System that runs Python) and in binary versions for OSX and Windows. web2py does not need to be installed. You unzip it, click on it, and choose a one-time administrator password. It then opens the browser for you and directs you to the administrative interface. Everything it needs to make this happen (the Python interpreter, the web-server, the relational database, etc.) is already packaged with web2py. If you need more power you customize your applications to use your preferred web-server (for example Apache) and your preferred database engine (for example PostgreSQL or Oracle).

- -

Via the admin interface you can upload a packed application, create a new application, design an application, maintain an existing application, bytecode-compile an application, pack and download an application. Everything can be done via the web-based admin interface, including editing the files that comprise your applications, clearing temp files, browsing past tickets/errors, run tests, interact with the database. If you so choose, it is also possible to interact with web2py via the Operating System shell or the Python shell.

- -

Any web2py application is comprised of Models (files that contain a description of the data representation), Views (files that contain a description of the data presentation), Controllers (files that contain a description of the business logic and workflow), Cron Jobs (tasks that need to be executed regularly in background), Modules (collections of reusable classes and functions), and Static Files (images, scripts, stylesheets, etc.). -

- -

Controllers consist of functions that are associated to a URL and are called when the associated URL is visited. Models are executed before the function is called, independently on the visited URL (for each app). Views are called when the function returns data other than a string, and renders the data in the proper format (for example html).

- -

-

A web2py application can be as simple as a single file (controllers/default.py) containing:

- - {{=CODE('def index(): return "Hello World"',counter=None,_class='boxCode')}} - -

When http://localhost:8000/app/default/index is visited the function is called and it displays the message "Hello World".

- -

Here is a more complex complete application that lets the visitor upload images into a database:

- - {{=CODE("""# in Model -db=DAL('sqlite://storage.db') -db.define_table('image', - Field('name'), - Field('file','upload'))""",counter=None,_class='boxCode')}} - {{=CODE("""# in Controller -def index(): - form = SQLFORM(db.image) - if form.accepts(request.vars, session): - response.flash = 'image uploaded' - return dict(form = form)""",counter=None,_class='boxCode')}} - - {{=CODE( - """# in View -[[extend 'layout.html']] -

Image upload form

-[[= form]]""".replace('[','{').replace(']','}'),counter=None,_class='boxCode')}} - -

Uploaded images are safely renamed to avoid directory traversal vulnerabilities, stored on the filesystem (or database) and a corresponding entry is inserted in the database, linking the file. A built-in mechanism prevents involuntary double form submission. All DB IO is transaction safe by default. Any exception in the code causes the transaction to rollback.

-
-

Examples of more complex sample applications can be found here

-
-
DELETED applications/examples/views/default/who.html Index: applications/examples/views/default/who.html ================================================================== --- applications/examples/views/default/who.html +++ applications/examples/views/default/who.html @@ -1,147 +0,0 @@ -{{extend 'layout.html'}} -
-

- The web2py™ Team -

-

- Lead Developer -

-
    -
  • - Massimo Di Pierro - (Associate Professor in Computer Science at DePaul University in Chicago) -
  • -
-

- Contributor Agreement -

-

- By contributing to web2py you implicitly agree to the - web2py contributor agreement. - Please also send us a signed copy by fax or, scanned, by email. -

-

-

- Main Contributors -

-
    - -
  • Alexey Nezhdanov (GAE and database performance) -
  • Alvaro Justen (dynamical translations) -
  • Anthony Bastardi (new poweredby site, LoadFactory) -
  • Arun K. Rajeevan (plugin_wiki) -
  • Attila Csipa (cron job) -
  • Bill Ferrett (modular DAL design) -
  • Boris Manojlovic (ajax edit) -
  • Branko Vukelic (new admin app) -
  • Brian Meredyk (SQLite and executesql) -
  • Bruno Rocha (book, new examples app, better forms) -
  • Carsten Haese (Informix) -
  • Chris Clark (Ingres, Jython support) -
  • Chris Steel -
  • Christian Foster Howes (GAE support) -
  • Christopher Smiga (Informix) -
  • CJ Lazell (tester) -
  • Craig Younkins (Security) -
  • Daniel Lin (Taiwanese internationalization) -
  • David Wagner (security and cryptography expert) -
  • Denes Lengyel (validators, DB2 support, DAL, custom forms, legacy table support) -
  • Douglas Soares de Andrade (2.4 and 2.6 compliance, docstrings) -
  • Eric Vicenti (email with ssl) -
  • Falko Krause (mysql support) -
  • Fran Boon (authorization and authentication) -
  • Francisco Gama (bug fixing) -
  • Fred Yanowski (XHTML compliance) -
  • Gilson Filho -
  • Graham Dumpleton (WSGI) -
  • Gyuris Szabolcs (PGP Mail) -
  • Hamdy Abdel-Badeea (crud) -
  • Hans Donner (GAE support, Google login, widgets, Sphinx documentation) -
  • Hans Murx (Database support) -
  • Hans C. v. Stockhausen (OpenID, Google Wave) -
  • Ian Reinhart Geiser (html helpers) -
  • Jonathan Benn (is_url validator and tests) -
  • Jonathan Lundell (multiple contributions) -
  • Josh Goldfoot (xaml/html sanitizer) -
  • Jose Jachuf (Firebird support) -
  • Josh Jaques (web2py_ajax) -
  • José Vicente de Sousa (Layout for examples app/ main website) -
  • Keith Yang (openid) -
  • Kyle Smith (javascript) -
  • Limodou (winservice) -
  • Lucas D'Ávila -
  • Marcel Leuthi (Oracle support) -
  • Marcel Hellkamp (Bottle developer, multiple web server support) -
  • Marcello Della Longa (italian translation) -
  • Mariano Reingart (pysoaplib) -
  • Mark Larsen (taskbar widget) -
  • Mark Moore (databases and daemon scripts) -
  • Markus Gritsch (bug fixing) -
  • Martin Hufsky (expressions in DAL) -
  • Martin Mulone (new welcome app) -
  • Mateusz Banach (stickers, IS_EMAIL, IS_IMAGE, contenttype) -
  • Michael Willis (shell) -
  • Michele Comitini (faceboook) -
  • Nathan Freeze (admin design, IS_STRONG, DAL features, web2pyslices.com) -
  • Niall Sweeny (MSSQL support) -
  • Niccolo Polo (epydoc) -
  • Nicolas Bruxer (memcache support) -
  • Ondrej Such (MSSQL support) -
  • Ovidio Marinho Falcao Neto -
  • Pai (internationalization) -
  • Patrick Breitenbach -
  • Phyo Arkar Lwin (web hosting and Jython tester) -
  • Pierre Thibault (Eclipse integration and custom import) -
  • Robin Bhattacharyya (Google App Engine support) -
  • Ross Peoples (MSSQL, multiple contributions) -
  • Ruijun Luo (a.k.a. Iceberg) (setup_exe.py) -
  • Ryan Seto (template.py) -
  • Scott Roberts (testing, book) -
  • Sergey Podlesnyi (Oracle and migrations tester) -
  • Sharriff Aina (tester and PyAMF integration) -
  • Sriram Durbha (book) -
  • Sterling Hankins (tester, book) -
  • Stuart Rackham (MSSQL support) -
  • Telman Yusupov (Oracle support) -
  • Thadeus Burgess (validators) -
  • Tim Michelsen (Sphinx documentation) -
  • Timothy Farrell (python 2.6 compliance, windows support) -
  • Yair Eshel (internationalizaiton) -
  • Yarko Tymciurak (design, Sphinx documentation) -
  • Younghyun Jo (internationalization) -
  • Vidul Nikolaev Petrov (captcha) -
  • Zahariash (memory management) -
  • - -
-
-
-

- Third party software included in web2py -

-
    - -
  • Python created by Guido van Rossum.
  • -
  • Rocket Web Server developed by Timothy Farrell.
  • -
  • EditArea developed by Christophe Dolivet
  • -
  • nicEdit developed by Brian Kirchoff
  • -
  • simplejson developed by Bob Ippolito
  • -
  • PyRTF developed by Simon Cusack and revised by Grant Edwards
  • -
  • PyRSS2Gen developed by Dalke Scientific Software
  • -
  • feedparser developed by Mark Pilgrim
  • -
  • markdown2 developed by Trent Mick
  • -
  • fcgi.py devloped by Allan Saddi (for production Lighttpd servers)
  • -
  • memcache developed by Evan Martin
  • -
  • jQuery developed by John Resig
  • -
  • A syntax highlighter inspired by the code of Peter Wilkinson
  • - -
-
-
- - - - - - - DELETED applications/examples/views/generic.html Index: applications/examples/views/generic.html ================================================================== --- applications/examples/views/generic.html +++ applications/examples/views/generic.html @@ -1,20 +0,0 @@ -{{extend 'layout.html'}} -{{""" - -You should not modify this file. -It is used as default when a view is not provided for your controllers - -"""}} - -{{=BEAUTIFY(response._vars)}} - - - - - - - - - - -{{block sidebar}}{{end}} DELETED applications/examples/views/generic.json Index: applications/examples/views/generic.json ================================================================== --- applications/examples/views/generic.json +++ applications/examples/views/generic.json @@ -1,15 +0,0 @@ -{{ -### -# response._vars contains the dictionary returned by the controller action -### -try: - from gluon.serializers import json - response.write(json(response._vars), escape=False) - response.headers['Content-Type'] = 'application/json' -except (TypeError, ValueError): - raise HTTP(405, 'JSON serialization error') -except ImportError: - raise HTTP(405, 'JSON not available') -except: - raise HTTP(405, 'JSON error') -}} DELETED applications/examples/views/generic.load Index: applications/examples/views/generic.load ================================================================== --- applications/examples/views/generic.load +++ applications/examples/views/generic.load @@ -1,1 +0,0 @@ -{{response.headers['web2py-response-flash']=response.flash}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}} DELETED applications/examples/views/generic.rss Index: applications/examples/views/generic.rss ================================================================== --- applications/examples/views/generic.rss +++ applications/examples/views/generic.rss @@ -1,20 +0,0 @@ -{{ -### -# response._vars contains the dictionary returned by the controller action -# for this to work the action must return something like -# -# dict(title=...,link=...,description=...,created_on='...',items=...) -# -# items is a list of dictionaries each with title, link, description, pub_date. -### -try: - from gluon.serializers import rss - response.write(rss(response._vars), escape=False) - response.headers['Content-Type'] = 'application/rss+xml' -except (TypeError, ValueError): - raise HTTP(405, 'RSS serialization error') -except ImportError: - raise HTTP(405, 'RSS not available') -except: - raise HTTP(405, 'RSS error') -}} DELETED applications/examples/views/generic.xml Index: applications/examples/views/generic.xml ================================================================== --- applications/examples/views/generic.xml +++ applications/examples/views/generic.xml @@ -1,15 +0,0 @@ -{{ -### -# response._vars contains the dictionary returned by thecontroller action -### -try: - from gluon.serializers import xml - response.write(xml(response._vars), escape=False) - response.headers['Content-Type'] = 'text/xml' -except (TypeError, ValueError): - raise HTTP(405, 'XML serialization error') -except ImportError: - raise HTTP(405, 'XML not available') -except: - raise HTTP(405, 'XML error') -}} DELETED applications/examples/views/global/vars.html Index: applications/examples/views/global/vars.html ================================================================== --- applications/examples/views/global/vars.html +++ applications/examples/views/global/vars.html @@ -1,47 +0,0 @@ -{{extend 'layout.html'}} -{{import cgi}} - -
-

{{=T('Docs for')}} {{=title}}

- -
- [ Python Tutorial ] - [ Python Libraries ] - [ web2py epydoc ] -
- -

{{=T('Description')}}

- -
- {{if t:}} - {{=t}}{{if d:}} extends {{=d}}{{pass}} - {{pass}} -
- {{pass}} - - {{if doc:}}

{{=CODE(str(doc),language=None,counter=None,_class='boxCode')}}{{pass}} -

-
-

{{=T('Attributes')}}

- - {{keys=attributes.keys(); keys.sort()}} - - - {{for a in keys:}} - {{doc1,t1,c1,d1=attributes[a]}} - - - - - - {{pass}} -

{{#=a}}{{=A(a,_href=URL(r=request,args=a.split('.')))}} - {{if t1:}} - {{=t1}}{{if d1:}} extends {{=d1}}{{pass}} - {{if c1:}} belongs to class {{=c1}}{{pass}} -
- {{pass}} - {{if doc1:}}{{=XML(cgi.escape(str(doc1)).replace(chr(13),'
'))}}{{pass}} -

-
-
DELETED applications/examples/views/images_examples/index.html Index: applications/examples/views/images_examples/index.html ================================================================== --- applications/examples/views/images_examples/index.html +++ applications/examples/views/images_examples/index.html @@ -1,5 +0,0 @@ -{{extend 'layout.html'}} -

Upload page

-{{=form}} - -{{block sidebar end}} DELETED applications/examples/views/layout.html Index: applications/examples/views/layout.html ================================================================== --- applications/examples/views/layout.html +++ applications/examples/views/layout.html @@ -1,138 +0,0 @@ - - - - {{=response.title or request.application}} - - - {{ - #---- (CSS) -----) - import random - response.files.append(URL('static','css/menu.css')) - response.files.append(URL('static','css/home.css')) - - #------ include web2py specific js code (jquery, calendar, form stuff) ------ - }} - {{include 'web2py_ajax.html'}} - {{=toggle_menuclass()}} - - - -
-
{{=response.flash or ''}}
-
- - - - DELETED applications/examples/views/layout_examples/basic.html Index: applications/examples/views/layout_examples/basic.html ================================================================== --- applications/examples/views/layout_examples/basic.html +++ applications/examples/views/layout_examples/basic.html @@ -1,3 +0,0 @@ -{{extend 'layout.html'}} -

{{=message}}

-{{for i in range(1000):}}bla {{pass}} DELETED applications/examples/views/layout_examples/civilized.html Index: applications/examples/views/layout_examples/civilized.html ================================================================== --- applications/examples/views/layout_examples/civilized.html +++ applications/examples/views/layout_examples/civilized.html @@ -1,3 +0,0 @@ -{{extend 'layout_examples/layout_civilized.html'}} -

{{=message}}

-

{{for i in range(1000):}}bla {{pass}} 

DELETED applications/examples/views/layout_examples/layout_civilized.html Index: applications/examples/views/layout_examples/layout_civilized.html ================================================================== --- applications/examples/views/layout_examples/layout_civilized.html +++ applications/examples/views/layout_examples/layout_civilized.html @@ -1,290 +0,0 @@ - - - - - -{{=request.application}} - - - - - -
- - {{if response.menu:}} - - {{pass}} -
-
-
- {{if response.flash:}}

FLASH: {{=response.flash}}

{{pass}} - {{include}} -
-
-
-
-
-
- -
- - DELETED applications/examples/views/layout_examples/layout_sleek.html Index: applications/examples/views/layout_examples/layout_sleek.html ================================================================== --- applications/examples/views/layout_examples/layout_sleek.html +++ applications/examples/views/layout_examples/layout_sleek.html @@ -1,252 +0,0 @@ - - - - - -{{=request.application}} - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- -
-{{if response.flash:}}

FLASH: {{=response.flash}}

{{pass}} -{{include}} -
- -
- - - - DELETED applications/examples/views/layout_examples/slick.html Index: applications/examples/views/layout_examples/slick.html ================================================================== --- applications/examples/views/layout_examples/slick.html +++ applications/examples/views/layout_examples/slick.html @@ -1,3 +0,0 @@ -{{extend 'layout_examples/layout_sleek.html'}} -

{{=message}}

-{{for i in range(1000):}}bla {{pass}} DELETED applications/examples/views/session_examples/counter.html Index: applications/examples/views/session_examples/counter.html ================================================================== --- applications/examples/views/session_examples/counter.html +++ applications/examples/views/session_examples/counter.html @@ -1,8 +0,0 @@ -{{extend 'layout.html'}} -

session counter

- -

{{for i in range(counter):}}{{=i}}... {{pass}}

- -{{=T('click me to count')}} - -{{block sidebar}} {{end}} DELETED applications/examples/views/simple_examples/hello3.html Index: applications/examples/views/simple_examples/hello3.html ================================================================== --- applications/examples/views/simple_examples/hello3.html +++ applications/examples/views/simple_examples/hello3.html @@ -1,2 +0,0 @@ -{{extend 'layout.html'}} -

{{=message}}

DELETED applications/examples/views/spreadsheet/index.html Index: applications/examples/views/spreadsheet/index.html ================================================================== --- applications/examples/views/spreadsheet/index.html +++ applications/examples/views/spreadsheet/index.html @@ -1,7 +0,0 @@ -{{extend 'layout.html'}} - -

Excel-like spreadsheet widget

- -Try insert "=r0c1+1" in cell r0c0 and "2" in r0c1. Formulas start with "=" as in Excel. You can use a subset of python commands and math function, and reference cells by r[row]c[col]. All computations are performed serverside via Ajax (input is validated for security). Cell values and formulas can be set and locked serverside. The shape of the spreadsheet can be modifed serverside and does not need to be tabular (think of it as a graph of css-friendly widgets you can place where you want). Cells can be given arbistrary names. This example is distributed with web2py so look at the source code of the example to learn more. - -{{=sheet}} DELETED applications/examples/views/template_examples/beautify.html Index: applications/examples/views/template_examples/beautify.html ================================================================== --- applications/examples/views/template_examples/beautify.html +++ applications/examples/views/template_examples/beautify.html @@ -1,5 +0,0 @@ -{{extend 'layout.html'}} -

BEAUTIFY

- -

Message is

-{{=message}} DELETED applications/examples/views/template_examples/escape.html Index: applications/examples/views/template_examples/escape.html ================================================================== --- applications/examples/views/template_examples/escape.html +++ applications/examples/views/template_examples/escape.html @@ -1,5 +0,0 @@ -{{extend 'layout.html'}} -

Strings are automatically escaped

- -

Message is

-{{=message}} DELETED applications/examples/views/template_examples/test_def.html Index: applications/examples/views/template_examples/test_def.html ================================================================== --- applications/examples/views/template_examples/test_def.html +++ applications/examples/views/template_examples/test_def.html @@ -1,7 +0,0 @@ -{{extend 'layout.html'}} -{{def itemlink(name):}}
  • {{=A(name,_href=name)}}
  • {{return}} -
      -{{itemlink('http://www.google.com')}} -{{itemlink('http://www.yahoo.com')}} -{{itemlink('http://www.nyt.com')}} -
    DELETED applications/examples/views/template_examples/test_for.html Index: applications/examples/views/template_examples/test_for.html ================================================================== --- applications/examples/views/template_examples/test_for.html +++ applications/examples/views/template_examples/test_for.html @@ -1,6 +0,0 @@ -{{extend 'layout.html'}} -

    For loop

    - -{{for number in ['one','two','three']:}} -

    {{=number.capitalize()}}

    -{{pass}} DELETED applications/examples/views/template_examples/test_if.html Index: applications/examples/views/template_examples/test_if.html ================================================================== --- applications/examples/views/template_examples/test_if.html +++ applications/examples/views/template_examples/test_if.html @@ -1,12 +0,0 @@ -{{extend 'layout.html'}} -

    If statement

    - -{{ -a=10 -}} - -{{if a%2==0:}} -

    {{=a}} is even

    -{{else:}} -

    {{=a}} is odd

    -{{pass}} DELETED applications/examples/views/template_examples/test_try.html Index: applications/examples/views/template_examples/test_try.html ================================================================== --- applications/examples/views/template_examples/test_try.html +++ applications/examples/views/template_examples/test_try.html @@ -1,8 +0,0 @@ -{{extend 'layout.html'}} -

    Try... except

    - -{{try:}} -

    a={{=1/0}}

    -{{except:}} - infinity -{{pass}} DELETED applications/examples/views/template_examples/variables.html Index: applications/examples/views/template_examples/variables.html ================================================================== --- applications/examples/views/template_examples/variables.html +++ applications/examples/views/template_examples/variables.html @@ -1,4 +0,0 @@ -{{extend 'layout.html'}} -

    Your variables

    -

    a={{=a}}

    -

    a={{=b}}

    DELETED applications/examples/views/template_examples/xml.html Index: applications/examples/views/template_examples/xml.html ================================================================== --- applications/examples/views/template_examples/xml.html +++ applications/examples/views/template_examples/xml.html @@ -1,5 +0,0 @@ -{{extend 'layout.html'}} -

    XML

    - -

    Message is

    -{{=message}} DELETED applications/examples/views/web2py_ajax.html Index: applications/examples/views/web2py_ajax.html ================================================================== --- applications/examples/views/web2py_ajax.html +++ applications/examples/views/web2py_ajax.html @@ -1,27 +0,0 @@ -{{ -response.files.insert(0,URL('static','js/jquery.js')) -response.files.insert(1,URL('static','css/calendar.css')) -response.files.insert(2,URL('static','js/calendar.js')) -for _item in response.meta or []:}} - {{ -pass -for _k,_file in enumerate(response.files or []): - if _file in response.files[:_k]: - continue - _file0=_file.lower().split('?')[0] - if _file0.endswith('.css'):}} - {{ - elif _file0.endswith('.js'):}} - {{ - pass -pass -}} - - - - Index: applications/mobileblur/controllers/default.py ================================================================== --- applications/mobileblur/controllers/default.py +++ applications/mobileblur/controllers/default.py @@ -7,7 +7,21 @@ for i in range(threshold, 2): if feed[thresholds[i]] > 0: feeds[feed["feed_title"]] = feed break - pprint(feeds) return dict(feeds=feeds, threshold=threshold) + + +def login(): + login_form = SQLFORM.factory( + Field("username", requires=IS_NOT_EMPTY()), + Field("password", "password", requires=IS_NOT_EMPTY()) + ) + if login_form.accepts(request): + results = newsblur.login(login_form.vars["username"], login_form.vars["password"]) + response.cookies["nb_cookie"] = newsblur.cookies + response.cookies["nb_cookie"]["path"] = "/" + print "cookie =", newsblur.cookies + redirect(URL("index")) + + return dict(login_form=login_form) Index: applications/mobileblur/models/0_helpers.py ================================================================== --- applications/mobileblur/models/0_helpers.py +++ applications/mobileblur/models/0_helpers.py @@ -1,9 +1,12 @@ newsblur = local_import("newsblur") threshold = 0 thresholds = ["nt", "ps", "ng"] # indices -1, 0, 1 for negative, neutral, and positive intelligence filters -def login(username="spiffytech"): - user = db(db.users.username==username).select().first() - if user["cookie"] is None: - results = newsblur.login(user["username"], user["password"]) +#import ipdb +#ipdb.set_trace() +if "nb_cookie" not in request.cookies.keys(): + if [request.application, request.controller, request.function] != [request.application, "default", "login"]: + redirect(URL("default", "login")) +#else: +# newsblur.cookies = request.cookies["nb_cookie"] Index: applications/mobileblur/models/db.py ================================================================== --- applications/mobileblur/models/db.py +++ applications/mobileblur/models/db.py @@ -32,10 +32,11 @@ ######################################################################### from gluon.tools import Mail, Auth, Crud, Service, PluginManager, prettydate mail = Mail() # mailer auth = Auth(db) # authentication/authorization + crud = Crud(db) # for CRUD helpers using auth service = Service() # for json, xml, jsonrpc, xmlrpc, amfrpc plugins = PluginManager() # for configuring plugins mail.settings.server = 'logging' or 'smtp.gmail.com:587' # your SMTP server @@ -84,6 +85,5 @@ db.define_table("users", Field("username"), Field("password"), Field("cookie") ) -login() ADDED applications/mobileblur/views/default/login.html Index: applications/mobileblur/views/default/login.html ================================================================== --- applications/mobileblur/views/default/login.html +++ applications/mobileblur/views/default/login.html @@ -0,0 +1,7 @@ +{{left_sidebar_enabled=right_sidebar_enabled=False}} +{{extend 'layout.html'}} + +{{= login_form }} + +{{block left_sidebar}}New Left Sidebar Content{{end}} +{{block right_sidebar}}New Right Sidebar Content{{end}} DELETED applications/welcome/ABOUT Index: applications/welcome/ABOUT ================================================================== --- applications/welcome/ABOUT +++ applications/welcome/ABOUT @@ -1,2 +0,0 @@ -Write something about this app. -Developed with web2py. DELETED applications/welcome/LICENSE Index: applications/welcome/LICENSE ================================================================== --- applications/welcome/LICENSE +++ applications/welcome/LICENSE @@ -1,4 +0,0 @@ -The web2py welcome app is licensed under public domain -(except for the css and js files that it includes, which have their own third party licenses). - -You can modify this license when you add your own code. DELETED applications/welcome/__init__.py Index: applications/welcome/__init__.py ================================================================== --- applications/welcome/__init__.py +++ applications/welcome/__init__.py DELETED applications/welcome/controllers/appadmin.py Index: applications/welcome/controllers/appadmin.py ================================================================== --- applications/welcome/controllers/appadmin.py +++ applications/welcome/controllers/appadmin.py @@ -1,408 +0,0 @@ -# -*- coding: utf-8 -*- - -# ########################################################## -# ## make sure administrator is on localhost -# ########################################################### - -import os -import socket -import datetime -import copy -import gluon.contenttype -import gluon.fileutils - -# ## critical --- make a copy of the environment - -global_env = copy.copy(globals()) -global_env['datetime'] = datetime - -http_host = request.env.http_host.split(':')[0] -remote_addr = request.env.remote_addr -try: - hosts = (http_host, socket.gethostname(), - socket.gethostbyname(http_host), - '::1','127.0.0.1','::ffff:127.0.0.1') -except: - hosts = (http_host, ) - -if request.env.http_x_forwarded_for or request.env.wsgi_url_scheme\ - in ['https', 'HTTPS']: - session.secure() -elif (remote_addr not in hosts) and (remote_addr != "127.0.0.1"): - raise HTTP(200, T('appadmin is disabled because insecure channel')) - -if (request.application=='admin' and not session.authorized) or \ - (request.application!='admin' and not gluon.fileutils.check_credentials(request)): - redirect(URL('admin', 'default', 'index')) - -ignore_rw = True -response.view = 'appadmin.html' -response.menu = [[T('design'), False, URL('admin', 'default', 'design', - args=[request.application])], [T('db'), False, - URL('index')], [T('state'), False, - URL('state')], [T('cache'), False, - URL('ccache')]] - -# ########################################################## -# ## auxiliary functions -# ########################################################### - - -def get_databases(request): - dbs = {} - for (key, value) in global_env.items(): - cond = False - try: - cond = isinstance(value, GQLDB) - except: - cond = isinstance(value, SQLDB) - if cond: - dbs[key] = value - return dbs - - -databases = get_databases(None) - - -def eval_in_global_env(text): - exec ('_ret=%s' % text, {}, global_env) - return global_env['_ret'] - - -def get_database(request): - if request.args and request.args[0] in databases: - return eval_in_global_env(request.args[0]) - else: - session.flash = T('invalid request') - redirect(URL('index')) - - -def get_table(request): - db = get_database(request) - if len(request.args) > 1 and request.args[1] in db.tables: - return (db, request.args[1]) - else: - session.flash = T('invalid request') - redirect(URL('index')) - - -def get_query(request): - try: - return eval_in_global_env(request.vars.query) - except Exception: - return None - - -def query_by_table_type(tablename,db,request=request): - keyed = hasattr(db[tablename],'_primarykey') - if keyed: - firstkey = db[tablename][db[tablename]._primarykey[0]] - cond = '>0' - if firstkey.type in ['string', 'text']: - cond = '!=""' - qry = '%s.%s.%s%s' % (request.args[0], request.args[1], firstkey.name, cond) - else: - qry = '%s.%s.id>0' % tuple(request.args[:2]) - return qry - - - -# ########################################################## -# ## list all databases and tables -# ########################################################### - - -def index(): - return dict(databases=databases) - - -# ########################################################## -# ## insert a new record -# ########################################################### - - -def insert(): - (db, table) = get_table(request) - form = SQLFORM(db[table], ignore_rw=ignore_rw) - if form.accepts(request.vars, session): - response.flash = T('new record inserted') - return dict(form=form,table=db[table]) - - -# ########################################################## -# ## list all records in table and insert new record -# ########################################################### - - -def download(): - import os - db = get_database(request) - return response.download(request,db) - -def csv(): - import gluon.contenttype - response.headers['Content-Type'] = \ - gluon.contenttype.contenttype('.csv') - db = get_database(request) - query = get_query(request) - if not query: - return None - response.headers['Content-disposition'] = 'attachment; filename=%s_%s.csv'\ - % tuple(request.vars.query.split('.')[:2]) - return str(db(query).select()) - - -def import_csv(table, file): - table.import_from_csv_file(file) - -def select(): - import re - db = get_database(request) - dbname = request.args[0] - regex = re.compile('(?P\w+)\.(?P\w+)=(?P\d+)') - if len(request.args)>1 and hasattr(db[request.args[1]],'_primarykey'): - regex = re.compile('(?P
    \w+)\.(?P\w+)=(?P.+)') - if request.vars.query: - match = regex.match(request.vars.query) - if match: - request.vars.query = '%s.%s.%s==%s' % (request.args[0], - match.group('table'), match.group('field'), - match.group('value')) - else: - request.vars.query = session.last_query - query = get_query(request) - if request.vars.start: - start = int(request.vars.start) - else: - start = 0 - nrows = 0 - stop = start + 100 - table = None - rows = [] - orderby = request.vars.orderby - if orderby: - orderby = dbname + '.' + orderby - if orderby == session.last_orderby: - if orderby[0] == '~': - orderby = orderby[1:] - else: - orderby = '~' + orderby - session.last_orderby = orderby - session.last_query = request.vars.query - form = FORM(TABLE(TR(T('Query:'), '', INPUT(_style='width:400px', - _name='query', _value=request.vars.query or '', - requires=IS_NOT_EMPTY(error_message=T("Cannot be empty")))), TR(T('Update:'), - INPUT(_name='update_check', _type='checkbox', - value=False), INPUT(_style='width:400px', - _name='update_fields', _value=request.vars.update_fields - or '')), TR(T('Delete:'), INPUT(_name='delete_check', - _class='delete', _type='checkbox', value=False), ''), - TR('', '', INPUT(_type='submit', _value='submit'))), - _action=URL(r=request,args=request.args)) - if request.vars.csvfile != None: - try: - import_csv(db[request.vars.table], - request.vars.csvfile.file) - response.flash = T('data uploaded') - except Exception, e: - response.flash = DIV(T('unable to parse csv file'),PRE(str(e))) - if form.accepts(request.vars, formname=None): -# regex = re.compile(request.args[0] + '\.(?P
    \w+)\.id\>0') - regex = re.compile(request.args[0] + '\.(?P
    \w+)\..+') - - match = regex.match(form.vars.query.strip()) - if match: - table = match.group('table') - try: - nrows = db(query).count() - if form.vars.update_check and form.vars.update_fields: - db(query).update(**eval_in_global_env('dict(%s)' - % form.vars.update_fields)) - response.flash = T('%s rows updated', nrows) - elif form.vars.delete_check: - db(query).delete() - response.flash = T('%s rows deleted', nrows) - nrows = db(query).count() - if orderby: - rows = db(query).select(limitby=(start, stop), - orderby=eval_in_global_env(orderby)) - else: - rows = db(query).select(limitby=(start, stop)) - except Exception, e: - (rows, nrows) = ([], 0) - response.flash = DIV(T('Invalid Query'),PRE(str(e))) - return dict( - form=form, - table=table, - start=start, - stop=stop, - nrows=nrows, - rows=rows, - query=request.vars.query, - ) - - -# ########################################################## -# ## edit delete one record -# ########################################################### - - -def update(): - (db, table) = get_table(request) - keyed = hasattr(db[table],'_primarykey') - record = None - if keyed: - key = [f for f in request.vars if f in db[table]._primarykey] - if key: - record = db(db[table][key[0]] == request.vars[key[0]]).select().first() - else: - record = db(db[table].id == request.args(2)).select().first() - - if not record: - qry = query_by_table_type(table, db) - session.flash = T('record does not exist') - redirect(URL('select', args=request.args[:1], - vars=dict(query=qry))) - - if keyed: - for k in db[table]._primarykey: - db[table][k].writable=False - - form = SQLFORM(db[table], record, deletable=True, delete_label=T('Check to delete'), - ignore_rw=ignore_rw and not keyed, - linkto=URL('select', - args=request.args[:1]), upload=URL(r=request, - f='download', args=request.args[:1])) - - if form.accepts(request.vars, session): - session.flash = T('done!') - qry = query_by_table_type(table, db) - redirect(URL('select', args=request.args[:1], - vars=dict(query=qry))) - return dict(form=form,table=db[table]) - - -# ########################################################## -# ## get global variables -# ########################################################### - - -def state(): - return dict() - -def ccache(): - form = FORM( - P(TAG.BUTTON("Clear CACHE?", _type="submit", _name="yes", _value="yes")), - P(TAG.BUTTON("Clear RAM", _type="submit", _name="ram", _value="ram")), - P(TAG.BUTTON("Clear DISK", _type="submit", _name="disk", _value="disk")), - ) - - if form.accepts(request.vars, session): - clear_ram = False - clear_disk = False - session.flash = "" - if request.vars.yes: - clear_ram = clear_disk = True - if request.vars.ram: - clear_ram = True - if request.vars.disk: - clear_disk = True - - if clear_ram: - cache.ram.clear() - session.flash += "Ram Cleared " - if clear_disk: - cache.disk.clear() - session.flash += "Disk Cleared" - - redirect(URL(r=request)) - - try: - from guppy import hpy; hp=hpy() - except ImportError: - hp = False - - import shelve, os, copy, time, math - from gluon import portalocker - - ram = { - 'bytes': 0, - 'objects': 0, - 'hits': 0, - 'misses': 0, - 'ratio': 0, - 'oldest': time.time() - } - disk = copy.copy(ram) - total = copy.copy(ram) - - for key, value in cache.ram.storage.items(): - if isinstance(value, dict): - ram['hits'] = value['hit_total'] - value['misses'] - ram['misses'] = value['misses'] - try: - ram['ratio'] = ram['hits'] * 100 / value['hit_total'] - except (KeyError, ZeroDivisionError): - ram['ratio'] = 0 - else: - if hp: - ram['bytes'] += hp.iso(value[1]).size - ram['objects'] += hp.iso(value[1]).count - - if value[0] < ram['oldest']: - ram['oldest'] = value[0] - - locker = open(os.path.join(request.folder, - 'cache/cache.lock'), 'a') - portalocker.lock(locker, portalocker.LOCK_EX) - disk_storage = shelve.open(os.path.join(request.folder, 'cache/cache.shelve')) - try: - for key, value in disk_storage.items(): - if isinstance(value, dict): - disk['hits'] = value['hit_total'] - value['misses'] - disk['misses'] = value['misses'] - try: - disk['ratio'] = disk['hits'] * 100 / value['hit_total'] - except (KeyError, ZeroDivisionError): - disk['ratio'] = 0 - else: - if hp: - disk['bytes'] += hp.iso(value[1]).size - disk['objects'] += hp.iso(value[1]).count - if value[0] < disk['oldest']: - disk['oldest'] = value[0] - finally: - portalocker.unlock(locker) - locker.close() - disk_storage.close() - - total['bytes'] = ram['bytes'] + disk['bytes'] - total['objects'] = ram['objects'] + disk['objects'] - total['hits'] = ram['hits'] + disk['hits'] - total['misses'] = ram['misses'] + disk['misses'] - try: - total['ratio'] = total['hits'] * 100 / (total['hits'] + total['misses']) - except (KeyError, ZeroDivisionError): - total['ratio'] = 0 - - if disk['oldest'] < ram['oldest']: - total['oldest'] = disk['oldest'] - else: - total['oldest'] = ram['oldest'] - - def GetInHMS(seconds): - hours = math.floor(seconds / 3600) - seconds -= hours * 3600 - minutes = math.floor(seconds / 60) - seconds -= minutes * 60 - seconds = math.floor(seconds) - - return (hours, minutes, seconds) - - ram['oldest'] = GetInHMS(time.time() - ram['oldest']) - disk['oldest'] = GetInHMS(time.time() - disk['oldest']) - total['oldest'] = GetInHMS(time.time() - total['oldest']) - - return dict(form=form, total=total, - ram=ram, disk=disk) - DELETED applications/welcome/controllers/default.py Index: applications/welcome/controllers/default.py ================================================================== --- applications/welcome/controllers/default.py +++ applications/welcome/controllers/default.py @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -# this file is released under public domain and you can use without limitations - -######################################################################### -## This is a samples controller -## - index is the default action of any application -## - user is required for authentication and authorization -## - download is for downloading files uploaded in the db (does streaming) -## - call exposes all registered services (none by default) -######################################################################### - -def index(): - """ - example action using the internationalization operator T and flash - rendered by views/default/index.html or views/generic.html - """ - return dict(message=T('Hello World')) - -def user(): - """ - exposes: - http://..../[app]/default/user/login - http://..../[app]/default/user/logout - http://..../[app]/default/user/register - http://..../[app]/default/user/profile - http://..../[app]/default/user/retrieve_password - http://..../[app]/default/user/change_password - use @auth.requires_login() - @auth.requires_membership('group name') - @auth.requires_permission('read','table name',record_id) - to decorate functions that need access control - """ - return dict(form=auth()) - - -def download(): - """ - allows downloading of uploaded files - http://..../[app]/default/download/[filename] - """ - return response.download(request,db) - - -def call(): - """ - exposes services. for example: - http://..../[app]/default/call/jsonrpc - decorate with @services.jsonrpc the functions to expose - supports xml, json, xmlrpc, jsonrpc, amfrpc, rss, csv - """ - return service() - - -@auth.requires_signature() -def data(): - """ - http://..../[app]/default/data/tables - http://..../[app]/default/data/create/[table] - http://..../[app]/default/data/read/[table]/[id] - http://..../[app]/default/data/update/[table]/[id] - http://..../[app]/default/data/delete/[table]/[id[ - http://..../[app]/default/data/select/[table] - http://..../[app]/default/data/search/[table] - but URLs bust be signed, i.e. linked with - A('table',_href=URL('data/tables',user_signature=True)) - or with the signed load operator - LOAD('default','data.load',args='tables',ajax=True,user_signature=True) - """ - return dict(form=crud()) DELETED applications/welcome/cron/crontab Index: applications/welcome/cron/crontab ================================================================== --- applications/welcome/cron/crontab +++ applications/welcome/cron/crontab @@ -1,1 +0,0 @@ -#crontab DELETED applications/welcome/languages/es-es.py Index: applications/welcome/languages/es-es.py ================================================================== --- applications/welcome/languages/es-es.py +++ applications/welcome/languages/es-es.py @@ -1,259 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"actualice" es una expresión opcional como "campo1=\'nuevo_valor\'". No se puede actualizar o eliminar resultados de un JOIN', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '%s filas eliminadas', -'%s rows updated': '%s filas actualizadas', -'(something like "it-it")': '(algo como "it-it")', -'A new version of web2py is available': 'Hay una nueva versión de web2py disponible', -'A new version of web2py is available: %s': 'Hay una nueva versión de web2py disponible: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENCION: Inicio de sesión requiere una conexión segura (HTTPS) o localhost.', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENCION: NO EJECUTE VARIAS PRUEBAS SIMULTANEAMENTE, NO SON THREAD SAFE.', -'ATTENTION: you cannot edit the running application!': 'ATENCION: no puede modificar la aplicación que se ejecuta!', -'About': 'Acerca de', -'About application': 'Acerca de la aplicación', -'Admin is disabled because insecure channel': 'Admin deshabilitado, el canal no es seguro', -'Admin is disabled because unsecure channel': 'Admin deshabilitado, el canal no es seguro', -'Administrator Password:': 'Contraseña del Administrador:', -'Are you sure you want to delete file "%s"?': '¿Está seguro que desea eliminar el archivo "%s"?', -'Are you sure you want to uninstall application "%s"': '¿Está seguro que desea desinstalar la aplicación "%s"', -'Are you sure you want to uninstall application "%s"?': '¿Está seguro que desea desinstalar la aplicación "%s"?', -'Authentication': 'Autenticación', -'Available databases and tables': 'Bases de datos y tablas disponibles', -'Cannot be empty': 'No puede estar vacío', -'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'No se puede compilar: hay errores en su aplicación. Depure, corrija errores y vuelva a intentarlo.', -'Change Password': 'Cambie Contraseña', -'Check to delete': 'Marque para eliminar', -'Client IP': 'IP del Cliente', -'Controller': 'Controlador', -'Controllers': 'Controladores', -'Copyright': 'Derechos de autor', -'Create new application': 'Cree una nueva aplicación', -'Current request': 'Solicitud en curso', -'Current response': 'Respuesta en curso', -'Current session': 'Sesión en curso', -'DB Model': 'Modelo "db"', -'DESIGN': 'DISEÑO', -'Database': 'Base de datos', -'Date and Time': 'Fecha y Hora', -'Delete': 'Elimine', -'Delete:': 'Elimine:', -'Deploy on Google App Engine': 'Instale en Google App Engine', -'Description': 'Descripción', -'Design for': 'Diseño para', -'E-mail': 'Correo electrónico', -'EDIT': 'EDITAR', -'Edit': 'Editar', -'Edit Profile': 'Editar Perfil', -'Edit This App': 'Edite esta App', -'Edit application': 'Editar aplicación', -'Edit current record': 'Edite el registro actual', -'Editing file': 'Editando archivo', -'Editing file "%s"': 'Editando archivo "%s"', -'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"', -'First name': 'Nombre', -'Functions with no doctests will result in [passed] tests.': 'Funciones sin doctests equivalen a pruebas [aceptadas].', -'Group ID': 'ID de Grupo', -'Hello World': 'Hola Mundo', -'Import/Export': 'Importar/Exportar', -'Index': 'Indice', -'Installed applications': 'Aplicaciones instaladas', -'Internal State': 'Estado Interno', -'Invalid Query': 'Consulta inválida', -'Invalid action': 'Acción inválida', -'Invalid email': 'Correo inválido', -'Language files (static strings) updated': 'Archivos de lenguaje (cadenas estáticas) actualizados', -'Languages': 'Lenguajes', -'Last name': 'Apellido', -'Last saved on:': 'Guardado en:', -'Layout': 'Diseño de página', -'License for': 'Licencia para', -'Login': 'Inicio de sesión', -'Login to the Administrative Interface': 'Inicio de sesión para la Interfaz Administrativa', -'Logout': 'Fin de sesión', -'Lost Password': 'Contraseña perdida', -'Main Menu': 'Menú principal', -'Menu Model': 'Modelo "menu"', -'Models': 'Modelos', -'Modules': 'Módulos', -'NO': 'NO', -'Name': 'Nombre', -'New Record': 'Registro nuevo', -'No databases in this application': 'No hay bases de datos en esta aplicación', -'Origin': 'Origen', -'Original/Translation': 'Original/Traducción', -'Password': 'Contraseña', -'Peeking at file': 'Visualizando archivo', -'Powered by': 'Este sitio usa', -'Query:': 'Consulta:', -'Record ID': 'ID de Registro', -'Register': 'Registrese', -'Registration key': 'Contraseña de Registro', -'Reset Password key': 'Reset Password key', -'Resolve Conflict file': 'archivo Resolución de Conflicto', -'Role': 'Rol', -'Rows in table': 'Filas en la tabla', -'Rows selected': 'Filas seleccionadas', -'Saved file hash:': 'Hash del archivo guardado:', -'Static files': 'Archivos estáticos', -'Stylesheet': 'Hoja de estilo', -'Sure you want to delete this object?': '¿Está seguro que desea eliminar este objeto?', -'Table name': 'Nombre de la tabla', -'Testing application': 'Probando aplicación', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "consulta" es una condición como "db.tabla1.campo1==\'valor\'". Algo como "db.tabla1.campo1==db.tabla2.campo2" resulta en un JOIN SQL.', -'The output of the file is a dictionary that was rendered by the view': 'La salida del archivo es un diccionario escenificado por la vista', -'There are no controllers': 'No hay controladores', -'There are no models': 'No hay modelos', -'There are no modules': 'No hay módulos', -'There are no static files': 'No hay archivos estáticos', -'There are no translators, only default language is supported': 'No hay traductores, sólo el lenguaje por defecto es soportado', -'There are no views': 'No hay vistas', -'This is a copy of the scaffolding application': 'Esta es una copia de la aplicación de andamiaje', -'This is the %(filename)s template': 'Esta es la plantilla %(filename)s', -'Ticket': 'Tiquete', -'Timestamp': 'Timestamp', -'Unable to check for upgrades': 'No es posible verificar la existencia de actualizaciones', -'Unable to download': 'No es posible la descarga', -'Unable to download app': 'No es posible descarga la aplicación', -'Update:': 'Actualice:', -'Upload existing application': 'Suba esta aplicación', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, y ~(...) para NOT, para crear consultas más complejas.', -'User ID': 'ID de Usuario', -'View': 'Vista', -'Views': 'Vistas', -'Welcome': 'Welcome', -'Welcome %s': 'Bienvenido %s', -'Welcome to web2py': 'Bienvenido a web2py', -'Which called the function': 'La cual llamó la función', -'YES': 'SI', -'You are successfully running web2py': 'Usted está ejecutando web2py exitosamente', -'You can modify this application and adapt it to your needs': 'Usted puede modificar esta aplicación y adaptarla a sus necesidades', -'You visited the url': 'Usted visitó la url', -'about': 'acerca de', -'additional code for your application': 'código adicional para su aplicación', -'admin disabled because no admin password': ' por falta de contraseña', -'admin disabled because not supported on google app engine': 'admin deshabilitado, no es soportado en GAE', -'admin disabled because unable to access password file': 'admin deshabilitado, imposible acceder al archivo con la contraseña', -'and rename it (required):': 'y renombrela (requerido):', -'and rename it:': ' y renombrelo:', -'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'admin deshabilitado, el canal no es seguro', -'application "%s" uninstalled': 'aplicación "%s" desinstalada', -'application compiled': 'aplicación compilada', -'application is compiled and cannot be designed': 'la aplicación está compilada y no puede ser modificada', -'cache': 'cache', -'cache, errors and sessions cleaned': 'cache, errores y sesiones eliminados', -'cannot create file': 'no es posible crear archivo', -'cannot upload file "%(filename)s"': 'no es posible subir archivo "%(filename)s"', -'change password': 'cambie contraseña', -'check all': 'marcar todos', -'clean': 'limpiar', -'Online examples': 'Ejemplos en línea', -'Administrative interface': 'Interfaz administrativa', -'click to check for upgrades': 'haga clic para buscar actualizaciones', -'compile': 'compilar', -'compiled application removed': 'aplicación compilada removida', -'controllers': 'controladores', -'create file with filename:': 'cree archivo con nombre:', -'create new application:': 'nombre de la nueva aplicación:', -'crontab': 'crontab', -'currently saved or': 'actualmente guardado o', -'customize me!': 'Adaptame!', -'data uploaded': 'datos subidos', -'database': 'base de datos', -'database %s select': 'selección en base de datos %s', -'database administration': 'administración base de datos', -'db': 'db', -'defines tables': 'define tablas', -'delete': 'eliminar', -'delete all checked': 'eliminar marcados', -'design': 'modificar', -'Documentation': 'Documentación', -'done!': 'listo!', -'edit': 'editar', -'edit controller': 'editar controlador', -'edit profile': 'editar perfil', -'errors': 'errores', -'export as csv file': 'exportar como archivo CSV', -'exposes': 'expone', -'extends': 'extiende', -'failed to reload module': 'recarga del módulo ha fallado', -'file "%(filename)s" created': 'archivo "%(filename)s" creado', -'file "%(filename)s" deleted': 'archivo "%(filename)s" eliminado', -'file "%(filename)s" uploaded': 'archivo "%(filename)s" subido', -'file "%(filename)s" was not deleted': 'archivo "%(filename)s" no fué eliminado', -'file "%s" of %s restored': 'archivo "%s" de %s restaurado', -'file changed on disk': 'archivo modificado en el disco', -'file does not exist': 'archivo no existe', -'file saved on %(time)s': 'archivo guardado %(time)s', -'file saved on %s': 'archivo guardado %s', -'help': 'ayuda', -'htmledit': 'htmledit', -'includes': 'incluye', -'insert new': 'inserte nuevo', -'insert new %s': 'inserte nuevo %s', -'internal error': 'error interno', -'invalid password': 'contraseña inválida', -'invalid request': 'solicitud inválida', -'invalid ticket': 'tiquete inválido', -'language file "%(filename)s" created/updated': 'archivo de lenguaje "%(filename)s" creado/actualizado', -'languages': 'lenguajes', -'languages updated': 'lenguajes actualizados', -'loading...': 'cargando...', -'located in the file': 'localizada en el archivo', -'login': 'inicio de sesión', -'logout': 'fin de sesión', -'lost password?': '¿olvido la contraseña?', -'merge': 'combinar', -'models': 'modelos', -'modules': 'módulos', -'new application "%s" created': 'nueva aplicación "%s" creada', -'new record inserted': 'nuevo registro insertado', -'next 100 rows': '100 filas siguientes', -'or import from csv file': 'o importar desde archivo CSV', -'or provide application url:': 'o provea URL de la aplicación:', -'pack all': 'empaquetar todo', -'pack compiled': 'empaquete compiladas', -'previous 100 rows': '100 filas anteriores', -'record': 'registro', -'record does not exist': 'el registro no existe', -'record id': 'id de registro', -'register': 'registrese', -'remove compiled': 'eliminar compiladas', -'restore': 'restaurar', -'revert': 'revertir', -'save': 'guardar', -'selected': 'seleccionado(s)', -'session expired': 'sesión expirada', -'shell': 'shell', -'site': 'sitio', -'some files could not be removed': 'algunos archivos no pudieron ser removidos', -'state': 'estado', -'static': 'estáticos', -'table': 'tabla', -'test': 'probar', -'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', -'the data representation, define database tables and sets': 'la representación de datos, define tablas y conjuntos de base de datos', -'the presentations layer, views are also known as templates': 'la capa de presentación, las vistas también son llamadas plantillas', -'these files are served without processing, your images go here': 'estos archivos son servidos sin procesar, sus imágenes van aquí', -'to previous version.': 'a la versión previa.', -'translation strings for the application': 'cadenas de caracteres de traducción para la aplicación', -'try': 'intente', -'try something like': 'intente algo como', -'unable to create application "%s"': 'no es posible crear la aplicación "%s"', -'unable to delete file "%(filename)s"': 'no es posible eliminar el archivo "%(filename)s"', -'unable to parse csv file': 'no es posible analizar el archivo CSV', -'unable to uninstall "%s"': 'no es posible instalar "%s"', -'uncheck all': 'desmarcar todos', -'uninstall': 'desinstalar', -'update': 'actualizar', -'update all languages': 'actualizar todos los lenguajes', -'upload application:': 'subir aplicación:', -'upload file:': 'suba archivo:', -'versioning': 'versiones', -'view': 'vista', -'views': 'vistas', -'web2py Recent Tweets': 'Tweets Recientes de web2py', -'web2py is up to date': 'web2py está actualizado', -} DELETED applications/welcome/languages/fr-ca.py Index: applications/welcome/languages/fr-ca.py ================================================================== --- applications/welcome/languages/fr-ca.py +++ applications/welcome/languages/fr-ca.py @@ -1,167 +0,0 @@ -# coding: utf8 -{ -'"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', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '%s rangées supprimées', -'%s rows updated': '%s rangées mises à jour', -'About': 'À propos', -'Access Control': "Contrôle d'accès", -'Administrative interface': "Interface d'administration", -'Ajax Recipes': 'Recettes Ajax', -'Are you sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?', -'Authentication': 'Authentification', -'Available databases and tables': 'Bases de données et tables disponibles', -'Buy this book': 'Acheter ce livre', -'Cannot be empty': 'Ne peut pas être vide', -'Check to delete': 'Cliquez pour supprimer', -'Check to delete:': 'Cliquez pour supprimer:', -'Client IP': 'IP client', -'Community': 'Communauté', -'Controller': 'Contrôleur', -'Copyright': "Droit d'auteur", -'Current request': 'Demande actuelle', -'Current response': 'Réponse actuelle', -'Current session': 'Session en cours', -'DB Model': 'Modèle DB', -'Database': 'Base de données', -'Delete:': 'Supprimer:', -'Demo': 'Démo', -'Deployment Recipes': 'Recettes de déploiement ', -'Description': 'Descriptif', -'Documentation': 'Documentation', -'Download': 'Téléchargement', -'E-mail': 'Courriel', -'Edit': 'Éditer', -'Edit This App': 'Modifier cette application', -'Edit current record': "Modifier l'enregistrement courant", -'Errors': 'Erreurs', -'FAQ': 'faq', -'First name': 'Prénom', -'Forms and Validators': 'Formulaires et Validateurs', -'Free Applications': 'Applications gratuites', -'Function disabled': 'Fonction désactivée', -'Group %(group_id)s created': '%(group_id)s groupe créé', -'Group ID': 'Groupe ID', -'Group uniquely assigned to user %(id)s': "Groupe unique attribué à l'utilisateur %(id)s", -'Groups': 'Groupes', -'Hello World': 'Bonjour le monde', -'Home': 'Accueil', -'Import/Export': 'Importer/Exporter', -'Index': 'Index', -'Internal State': 'État interne', -'Introduction': 'Présentation', -'Invalid Query': 'Requête Invalide', -'Invalid email': 'Courriel invalide', -'Last name': 'Nom', -'Layout': 'Mise en page', -'Layouts': 'layouts', -'Live chat': 'Clavardage en direct', -'Logged in': 'Connecté', -'Login': 'Connectez-vous', -'Lost Password': 'Mot de passe perdu', -'Main Menu': 'Menu principal', -'Menu Model': 'Menu modèle', -'Name': 'Nom', -'New Record': 'Nouvel enregistrement', -'No databases in this application': "Cette application n'a pas de bases de données", -'Online examples': 'Exemples en ligne', -'Origin': 'Origine', -'Other Recipes': 'Autres recettes', -'Overview': 'Présentation', -'Password': 'Mot de passe', -"Password fields don't match": 'Les mots de passe ne correspondent pas', -'Plugins': 'Plugiciels', -'Powered by': 'Alimenté par', -'Preface': 'Préface', -'Python': 'Python', -'Query:': 'Requête:', -'Quick Examples': 'Examples Rapides', -'Readme': 'Lisez-moi', -'Recipes': 'Recettes', -'Record %(id)s created': 'Record %(id)s created', -'Record %(id)s updated': 'Record %(id)s updated', -'Record Created': 'Record Created', -'Record ID': "ID d'enregistrement", -'Record Updated': 'Record Updated', -'Register': "S'inscrire", -'Registration key': "Clé d'enregistrement", -'Registration successful': 'Inscription réussie', -'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)', -'Request reset password': 'Demande de réinitialiser le mot clé', -'Reset Password key': 'Réinitialiser le mot clé', -'Resources': 'Ressources', -'Role': 'Rôle', -'Rows in table': 'Lignes du tableau', -'Rows selected': 'Lignes sélectionnées', -'Semantic': 'Sémantique', -'Services': 'Services', -'Stylesheet': 'Feuille de style', -'Submit': 'Soumettre', -'Support': 'Soutien', -'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?', -'Table name': 'Nom du tableau', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "query" est une condition comme "db.table1.champ1==\'valeur\'". Quelque chose comme "db.table1.champ1==db.table2.champ2" résulte en un JOIN SQL.', -'The Core': 'Le noyau', -'The Views': 'Les Vues', -'The output of the file is a dictionary that was rendered by the view': 'La sortie de ce fichier est un dictionnaire qui été restitué par la vue', -'This App': 'Cette Appli', -'This is a copy of the scaffolding application': "Ceci est une copie de l'application échafaudage", -'Timestamp': 'Horodatage', -'Twitter': 'Twitter', -'Update:': 'Mise à jour:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT pour construire des requêtes plus complexes.', -'User %(id)s Logged-in': 'Utilisateur %(id)s connecté', -'User %(id)s Registered': 'Utilisateur %(id)s enregistré', -'User ID': 'ID utilisateur', -'User Voice': 'User Voice', -'Verify Password': 'Vérifiez le mot de passe', -'Videos': 'Vidéos', -'View': 'Présentation', -'Web2py': 'Web2py', -'Welcome': 'Bienvenu', -'Welcome %s': 'Bienvenue %s', -'Welcome to web2py': 'Bienvenue à web2py', -'Which called the function': 'Qui a appelé la fonction', -'You are successfully running web2py': 'Vous roulez avec succès web2py', -'You can modify this application and adapt it to your needs': "Vous pouvez modifier cette application et l'adapter à vos besoins", -'You visited the url': "Vous avez visité l'URL", -'about': 'à propos', -'appadmin is disabled because insecure channel': "appadmin est désactivée parce que le canal n'est pas sécurisé", -'cache': 'cache', -'change password': 'changer le mot de passe', -'customize me!': 'personnalisez-moi!', -'data uploaded': 'données téléchargées', -'database': 'base de données', -'database %s select': 'base de données %s select', -'db': 'db', -'design': 'design', -'done!': 'fait!', -'edit profile': 'modifier le profil', -'enter an integer between %(min)g and %(max)g': 'entrer un entier compris entre %(min)g et %(max)g', -'export as csv file': 'exporter sous forme de fichier csv', -'insert new': 'insérer un nouveau', -'insert new %s': 'insérer un nouveau %s', -'invalid request': 'requête invalide', -'located in the file': 'se trouvant dans le fichier', -'login': 'connectez-vous', -'logout': 'déconnectez-vous', -'lost password': 'mot de passe perdu', -'lost password?': 'mot de passe perdu?', -'new record inserted': 'nouvel enregistrement inséré', -'next 100 rows': '100 prochaines lignes', -'or import from csv file': "ou importer d'un fichier CSV", -'password': 'mot de passe', -'please input your password again': "S'il vous plaît entrer votre mot de passe", -'previous 100 rows': '100 lignes précédentes', -'profile': 'profile', -'record': 'enregistrement', -'record does not exist': "l'archive n'existe pas", -'record id': "id d'enregistrement", -'register': "s'inscrire", -'selected': 'sélectionné', -'state': 'état', -'table': 'tableau', -'unable to parse csv file': "incapable d'analyser le fichier cvs", -'value already in database or empty': 'valeur déjà dans la base ou vide', -} DELETED applications/welcome/languages/fr-fr.py Index: applications/welcome/languages/fr-fr.py ================================================================== --- applications/welcome/languages/fr-fr.py +++ applications/welcome/languages/fr-fr.py @@ -1,155 +0,0 @@ -# coding: utf8 -{ -'"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', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '%s rangées supprimées', -'%s rows updated': '%s rangées mises à jour', -'About': 'À propos', -'Access Control': 'Contrôle d\'accès', -'Ajax Recipes': 'Recettes Ajax', -'Are you sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?', -'Authentication': 'Authentification', -'Available databases and tables': 'Bases de données et tables disponibles', -'Buy this book': 'Acheter ce livre', -'Cannot be empty': 'Ne peut pas être vide', -'Check to delete': 'Cliquez pour supprimer', -'Check to delete:': 'Cliquez pour supprimer:', -'Client IP': 'IP client', -'Community': 'Communauté', -'Controller': 'Contrôleur', -'Copyright': 'Copyright', -'Current request': 'Demande actuelle', -'Current response': 'Réponse actuelle', -'Current session': 'Session en cours', -'DB Model': 'Modèle DB', -'Database': 'Base de données', -'Delete:': 'Supprimer:', -'Demo': 'Démo', -'Deployment Recipes': 'Recettes de déploiement', -'Description': 'Description', -'Documentation': 'Documentation', -'Download': 'Téléchargement', -'E-mail': 'E-mail', -'Edit': 'Éditer', -'Edit This App': 'Modifier cette application', -'Edit current record': "Modifier l'enregistrement courant", -'Errors': 'Erreurs', -'FAQ': 'FAQ', -'First name': 'Prénom', -'Forms and Validators': 'Formulaires et Validateurs', -'Free Applications': 'Applications gratuites', -'Function disabled': 'Fonction désactivée', -'Group ID': 'Groupe ID', -'Groups': 'Groups', -'Hello World': 'Bonjour le monde', -'Home': 'Accueil', -'Import/Export': 'Importer/Exporter', -'Index': 'Index', -'Internal State': 'État interne', -'Introduction': 'Introduction', -'Invalid Query': 'Requête Invalide', -'Invalid email': 'E-mail invalide', -'Last name': 'Nom', -'Layout': 'Mise en page', -'Layouts': 'Layouts', -'Live chat': 'Chat live', -'Login': 'Connectez-vous', -'Lost Password': 'Mot de passe perdu', -'Main Menu': 'Menu principal', -'Menu Model': 'Menu modèle', -'Name': 'Nom', -'New Record': 'Nouvel enregistrement', -'No databases in this application': "Cette application n'a pas de bases de données", -'Origin': 'Origine', -'Other Recipes': 'Autres recettes', -'Overview': 'Présentation', -'Password': 'Mot de passe', -"Password fields don't match": 'Les mots de passe ne correspondent pas', -'Plugins': 'Plugiciels', -'Powered by': 'Alimenté par', -'Preface': 'Préface', -'Python': 'Python', -'Query:': 'Requête:', -'Quick Examples': 'Examples Rapides', -'Recipes': 'Recettes', -'Record ID': 'ID d\'enregistrement', -'Register': "S'inscrire", -'Registration key': "Clé d'enregistrement", -'Remember me (for 30 days)': 'Se souvenir de moi (pendant 30 jours)', -'Request reset password': 'Demande de réinitialiser le mot clé', -'Reset Password key': 'Réinitialiser le mot clé', -'Resources': 'Ressources', -'Role': 'Rôle', -'Rows in table': 'Lignes du tableau', -'Rows selected': 'Lignes sélectionnées', -'Semantic': 'Sémantique', -'Services': 'Services', -'Stylesheet': 'Feuille de style', -'Submit': 'Soumettre', -'Support': 'Support', -'Sure you want to delete this object?': 'Êtes-vous sûr de vouloir supprimer cet objet?', -'Table name': 'Nom du tableau', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "query" est une condition comme "db.table1.champ1==\'valeur\'". Quelque chose comme "db.table1.champ1==db.table2.champ2" résulte en un JOIN SQL.', -'The Core': 'Le noyau', -'The Views': 'Les Vues', -'The output of the file is a dictionary that was rendered by the view': 'La sortie de ce fichier est un dictionnaire qui été restitué par la vue', -'This App': 'Cette Appli', -'This is a copy of the scaffolding application': 'Ceci est une copie de l\'application échafaudage', -'Timestamp': 'Horodatage', -'Twitter': 'Twitter', -'Update:': 'Mise à jour:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT pour construire des requêtes plus complexes.', -'User %(id)s Logged-in': 'Utilisateur %(id)s connecté', -'User %(id)s Registered': 'Utilisateur %(id)s enregistré', -'User ID': 'ID utilisateur', -'User Voice': 'User Voice', -'Verify Password': 'Vérifiez le mot de passe', -'Videos': 'Vidéos', -'View': 'Présentation', -'Web2py': 'Web2py', -'Welcome': 'Bienvenu', -'Welcome %s': 'Bienvenue %s', -'Welcome to web2py': 'Bienvenue à web2py', -'Which called the function': 'Qui a appelé la fonction', -'You are successfully running web2py': 'Vous roulez avec succès web2py', -'You can modify this application and adapt it to your needs': 'Vous pouvez modifier cette application et l\'adapter à vos besoins', -'You visited the url': 'Vous avez visité l\'URL', -'appadmin is disabled because insecure channel': "appadmin est désactivée parce que le canal n'est pas sécurisé", -'cache': 'cache', -'change password': 'changer le mot de passe', -'Online examples': 'Exemples en ligne', -'Administrative interface': "Interface d'administration", -'customize me!': 'personnalisez-moi!', -'data uploaded': 'données téléchargées', -'database': 'base de données', -'database %s select': 'base de données %s select', -'db': 'db', -'design': 'design', -'Documentation': 'Documentation', -'done!': 'fait!', -'edit profile': 'modifier le profil', -'enter an integer between %(min)g and %(max)g': 'enter an integer between %(min)g and %(max)g', -'export as csv file': 'exporter sous forme de fichier csv', -'insert new': 'insérer un nouveau', -'insert new %s': 'insérer un nouveau %s', -'invalid request': 'requête invalide', -'located in the file': 'se trouvant dans le fichier', -'login': 'connectez-vous', -'logout': 'déconnectez-vous', -'lost password': 'mot de passe perdu', -'lost password?': 'mot de passe perdu?', -'new record inserted': 'nouvel enregistrement inséré', -'next 100 rows': '100 prochaines lignes', -'or import from csv file': "ou importer d'un fichier CSV", -'previous 100 rows': '100 lignes précédentes', -'record': 'enregistrement', -'record does not exist': "l'archive n'existe pas", -'record id': "id d'enregistrement", -'register': "s'inscrire", -'selected': 'sélectionné', -'state': 'état', -'table': 'tableau', -'unable to parse csv file': "incapable d'analyser le fichier cvs", -'Readme': "Lisez-moi", -} DELETED applications/welcome/languages/hi-hi.py Index: applications/welcome/languages/hi-hi.py ================================================================== --- applications/welcome/languages/hi-hi.py +++ applications/welcome/languages/hi-hi.py @@ -1,82 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '%s \xe0\xa4\xaa\xe0\xa4\x82\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\x81 \xe0\xa4\xae\xe0\xa4\xbf\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\x8f\xe0\xa4\x81', -'%s rows updated': '%s \xe0\xa4\xaa\xe0\xa4\x82\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\x81 \xe0\xa4\x85\xe0\xa4\xa6\xe0\xa5\x8d\xe0\xa4\xaf\xe0\xa4\xa4\xe0\xa4\xa8', -'Available databases and tables': '\xe0\xa4\x89\xe0\xa4\xaa\xe0\xa4\xb2\xe0\xa4\xac\xe0\xa5\x8d\xe0\xa4\xa7 \xe0\xa4\xa1\xe0\xa5\x87\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\xac\xe0\xa5\x87\xe0\xa4\xb8 \xe0\xa4\x94\xe0\xa4\xb0 \xe0\xa4\xa4\xe0\xa4\xbe\xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x95\xe0\xa4\xbe', -'Cannot be empty': '\xe0\xa4\x96\xe0\xa4\xbe\xe0\xa4\xb2\xe0\xa5\x80 \xe0\xa4\xa8\xe0\xa4\xb9\xe0\xa5\x80\xe0\xa4\x82 \xe0\xa4\xb9\xe0\xa5\x8b \xe0\xa4\xb8\xe0\xa4\x95\xe0\xa4\xa4\xe0\xa4\xbe', -'Change Password': '\xe0\xa4\xaa\xe0\xa4\xbe\xe0\xa4\xb8\xe0\xa4\xb5\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa1 \xe0\xa4\xac\xe0\xa4\xa6\xe0\xa4\xb2\xe0\xa5\x87\xe0\xa4\x82', -'Check to delete': '\xe0\xa4\xb9\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\xa8\xe0\xa5\x87 \xe0\xa4\x95\xe0\xa5\x87 \xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x8f \xe0\xa4\x9a\xe0\xa5\x81\xe0\xa4\xa8\xe0\xa5\x87\xe0\xa4\x82', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Current request': '\xe0\xa4\xb5\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xae\xe0\xa4\xbe\xe0\xa4\xa8 \xe0\xa4\x85\xe0\xa4\xa8\xe0\xa5\x81\xe0\xa4\xb0\xe0\xa5\x8b\xe0\xa4\xa7', -'Current response': '\xe0\xa4\xb5\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xae\xe0\xa4\xbe\xe0\xa4\xa8 \xe0\xa4\xaa\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa4\xa4\xe0\xa4\xbf\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe', -'Current session': '\xe0\xa4\xb5\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xae\xe0\xa4\xbe\xe0\xa4\xa8 \xe0\xa4\xb8\xe0\xa5\x87\xe0\xa4\xb6\xe0\xa4\xa8', -'DB Model': 'DB Model', -'Database': 'Database', -'Delete:': '\xe0\xa4\xae\xe0\xa4\xbf\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\xa8\xe0\xa4\xbe:', -'Edit': 'Edit', -'Edit Profile': '\xe0\xa4\xaa\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa5\x8b\xe0\xa4\xab\xe0\xa4\xbc\xe0\xa4\xbe\xe0\xa4\x87\xe0\xa4\xb2 \xe0\xa4\xb8\xe0\xa4\x82\xe0\xa4\xaa\xe0\xa4\xbe\xe0\xa4\xa6\xe0\xa4\xbf\xe0\xa4\xa4 \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x82', -'Edit This App': 'Edit This App', -'Edit current record': '\xe0\xa4\xb5\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xae\xe0\xa4\xbe\xe0\xa4\xa8 \xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x95\xe0\xa5\x89\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa1 \xe0\xa4\xb8\xe0\xa4\x82\xe0\xa4\xaa\xe0\xa4\xbe\xe0\xa4\xa6\xe0\xa4\xbf\xe0\xa4\xa4 \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x82 ', -'Hello World': 'Hello World', -'Hello from MyApp': 'Hello from MyApp', -'Import/Export': '\xe0\xa4\x86\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\xa4 / \xe0\xa4\xa8\xe0\xa4\xbf\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\xa4', -'Index': 'Index', -'Internal State': '\xe0\xa4\x86\xe0\xa4\x82\xe0\xa4\xa4\xe0\xa4\xb0\xe0\xa4\xbf\xe0\xa4\x95 \xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\xa5\xe0\xa4\xbf\xe0\xa4\xa4\xe0\xa4\xbf', -'Invalid Query': '\xe0\xa4\x85\xe0\xa4\xae\xe0\xa4\xbe\xe0\xa4\xa8\xe0\xa5\x8d\xe0\xa4\xaf \xe0\xa4\xaa\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa4\xb6\xe0\xa5\x8d\xe0\xa4\xa8', -'Layout': 'Layout', -'Login': '\xe0\xa4\xb2\xe0\xa5\x89\xe0\xa4\x97 \xe0\xa4\x87\xe0\xa4\xa8', -'Logout': '\xe0\xa4\xb2\xe0\xa5\x89\xe0\xa4\x97 \xe0\xa4\x86\xe0\xa4\x89\xe0\xa4\x9f', -'Lost Password': '\xe0\xa4\xaa\xe0\xa4\xbe\xe0\xa4\xb8\xe0\xa4\xb5\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa1 \xe0\xa4\x96\xe0\xa5\x8b \xe0\xa4\x97\xe0\xa4\xaf\xe0\xa4\xbe', -'Main Menu': 'Main Menu', -'Menu Model': 'Menu Model', -'New Record': '\xe0\xa4\xa8\xe0\xa4\xaf\xe0\xa4\xbe \xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x95\xe0\xa5\x89\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa1', -'No databases in this application': '\xe0\xa4\x87\xe0\xa4\xb8 \xe0\xa4\x85\xe0\xa4\xa8\xe0\xa5\x81\xe0\xa4\xaa\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa4\xaf\xe0\xa5\x8b\xe0\xa4\x97 \xe0\xa4\xae\xe0\xa5\x87\xe0\xa4\x82 \xe0\xa4\x95\xe0\xa5\x8b\xe0\xa4\x88 \xe0\xa4\xa1\xe0\xa5\x87\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\xac\xe0\xa5\x87\xe0\xa4\xb8 \xe0\xa4\xa8\xe0\xa4\xb9\xe0\xa5\x80\xe0\xa4\x82 \xe0\xa4\xb9\xe0\xa5\x88\xe0\xa4\x82', -'Powered by': 'Powered by', -'Query:': '\xe0\xa4\xaa\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa4\xb6\xe0\xa5\x8d\xe0\xa4\xa8:', -'Register': '\xe0\xa4\xaa\xe0\xa4\x82\xe0\xa4\x9c\xe0\xa5\x80\xe0\xa4\x95\xe0\xa5\x83\xe0\xa4\xa4 (\xe0\xa4\xb0\xe0\xa4\x9c\xe0\xa4\xbf\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\x9f\xe0\xa4\xb0) \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa4\xa8\xe0\xa4\xbe ', -'Rows in table': '\xe0\xa4\xa4\xe0\xa4\xbe\xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x95\xe0\xa4\xbe \xe0\xa4\xae\xe0\xa5\x87\xe0\xa4\x82 \xe0\xa4\xaa\xe0\xa4\x82\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\x81 ', -'Rows selected': '\xe0\xa4\x9a\xe0\xa4\xaf\xe0\xa4\xa8\xe0\xa4\xbf\xe0\xa4\xa4 (\xe0\xa4\x9a\xe0\xa5\x81\xe0\xa4\xa8\xe0\xa5\x87 \xe0\xa4\x97\xe0\xa4\xaf\xe0\xa5\x87) \xe0\xa4\xaa\xe0\xa4\x82\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\x81 ', -'Stylesheet': 'Stylesheet', -'Sure you want to delete this object?': '\xe0\xa4\xb8\xe0\xa5\x81\xe0\xa4\xa8\xe0\xa4\xbf\xe0\xa4\xb6\xe0\xa5\x8d\xe0\xa4\x9a\xe0\xa4\xbf\xe0\xa4\xa4 \xe0\xa4\xb9\xe0\xa5\x88\xe0\xa4\x82 \xe0\xa4\x95\xe0\xa4\xbf \xe0\xa4\x86\xe0\xa4\xaa \xe0\xa4\x87\xe0\xa4\xb8 \xe0\xa4\xb5\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa5\x81 \xe0\xa4\x95\xe0\xa5\x8b \xe0\xa4\xb9\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\xa8\xe0\xa4\xbe \xe0\xa4\x9a\xe0\xa4\xbe\xe0\xa4\xb9\xe0\xa4\xa4\xe0\xa5\x87 \xe0\xa4\xb9\xe0\xa5\x88\xe0\xa4\x82?', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', -'Update:': '\xe0\xa4\x85\xe0\xa4\xa6\xe0\xa5\x8d\xe0\xa4\xaf\xe0\xa4\xa4\xe0\xa4\xa8 \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa4\xa8\xe0\xa4\xbe:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', -'View': 'View', -'Welcome %s': 'Welcome %s', -'Welcome to web2py': '\xe0\xa4\xb5\xe0\xa5\x87\xe0\xa4\xac\xe0\xa5\xa8\xe0\xa4\xaa\xe0\xa4\xbe\xe0\xa4\x87 (web2py) \xe0\xa4\xae\xe0\xa5\x87\xe0\xa4\x82 \xe0\xa4\x86\xe0\xa4\xaa\xe0\xa4\x95\xe0\xa4\xbe \xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\xb5\xe0\xa4\xbe\xe0\xa4\x97\xe0\xa4\xa4 \xe0\xa4\xb9\xe0\xa5\x88', -'appadmin is disabled because insecure channel': '\xe0\xa4\x85\xe0\xa4\xaa \xe0\xa4\x86\xe0\xa4\xa1\xe0\xa4\xae\xe0\xa4\xbf\xe0\xa4\xa8 (appadmin) \xe0\xa4\x85\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xb7\xe0\xa4\xae \xe0\xa4\xb9\xe0\xa5\x88 \xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xaf\xe0\xa5\x8b\xe0\xa4\x82\xe0\xa4\x95\xe0\xa4\xbf \xe0\xa4\x85\xe0\xa4\xb8\xe0\xa5\x81\xe0\xa4\xb0\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xb7\xe0\xa4\xbf\xe0\xa4\xa4 \xe0\xa4\x9a\xe0\xa5\x88\xe0\xa4\xa8\xe0\xa4\xb2', -'cache': 'cache', -'change password': 'change password', -'Online examples': '\xe0\xa4\x91\xe0\xa4\xa8\xe0\xa4\xb2\xe0\xa4\xbe\xe0\xa4\x87\xe0\xa4\xa8 \xe0\xa4\x89\xe0\xa4\xa6\xe0\xa4\xbe\xe0\xa4\xb9\xe0\xa4\xb0\xe0\xa4\xa3 \xe0\xa4\x95\xe0\xa5\x87 \xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x8f \xe0\xa4\xaf\xe0\xa4\xb9\xe0\xa4\xbe\xe0\xa4\x81 \xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x95 \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x82', -'Administrative interface': '\xe0\xa4\xaa\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa4\xb6\xe0\xa4\xbe\xe0\xa4\xb8\xe0\xa4\xa8\xe0\xa4\xbf\xe0\xa4\x95 \xe0\xa4\x87\xe0\xa4\x82\xe0\xa4\x9f\xe0\xa4\xb0\xe0\xa4\xab\xe0\xa5\x87\xe0\xa4\xb8 \xe0\xa4\x95\xe0\xa5\x87 \xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x8f \xe0\xa4\xaf\xe0\xa4\xb9\xe0\xa4\xbe\xe0\xa4\x81 \xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x95 \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x82', -'customize me!': '\xe0\xa4\xae\xe0\xa5\x81\xe0\xa4\x9d\xe0\xa5\x87 \xe0\xa4\x85\xe0\xa4\xa8\xe0\xa5\x81\xe0\xa4\x95\xe0\xa5\x82\xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\xa4 (\xe0\xa4\x95\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\x9f\xe0\xa4\xae\xe0\xa4\xbe\xe0\xa4\x87\xe0\xa4\x9c\xe0\xa4\xbc) \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x82!', -'data uploaded': '\xe0\xa4\xa1\xe0\xa4\xbe\xe0\xa4\x9f\xe0\xa4\xbe \xe0\xa4\x85\xe0\xa4\xaa\xe0\xa4\xb2\xe0\xa5\x8b\xe0\xa4\xa1 \xe0\xa4\xb8\xe0\xa4\xae\xe0\xa5\x8d\xe0\xa4\xaa\xe0\xa4\xa8\xe0\xa5\x8d\xe0\xa4\xa8 ', -'database': '\xe0\xa4\xa1\xe0\xa5\x87\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\xac\xe0\xa5\x87\xe0\xa4\xb8', -'database %s select': '\xe0\xa4\xa1\xe0\xa5\x87\xe0\xa4\x9f\xe0\xa4\xbe\xe0\xa4\xac\xe0\xa5\x87\xe0\xa4\xb8 %s \xe0\xa4\x9a\xe0\xa5\x81\xe0\xa4\xa8\xe0\xa5\x80 \xe0\xa4\xb9\xe0\xa5\x81\xe0\xa4\x88', -'db': 'db', -'design': '\xe0\xa4\xb0\xe0\xa4\x9a\xe0\xa4\xa8\xe0\xa4\xbe \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x82', -'done!': '\xe0\xa4\xb9\xe0\xa5\x8b \xe0\xa4\x97\xe0\xa4\xaf\xe0\xa4\xbe!', -'edit profile': 'edit profile', -'export as csv file': 'csv \xe0\xa4\xab\xe0\xa4\xbc\xe0\xa4\xbe\xe0\xa4\x87\xe0\xa4\xb2 \xe0\xa4\x95\xe0\xa5\x87 \xe0\xa4\xb0\xe0\xa5\x82\xe0\xa4\xaa \xe0\xa4\xae\xe0\xa5\x87\xe0\xa4\x82 \xe0\xa4\xa8\xe0\xa4\xbf\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\xa4', -'insert new': '\xe0\xa4\xa8\xe0\xa4\xaf\xe0\xa4\xbe \xe0\xa4\xa1\xe0\xa4\xbe\xe0\xa4\xb2\xe0\xa5\x87\xe0\xa4\x82', -'insert new %s': '\xe0\xa4\xa8\xe0\xa4\xaf\xe0\xa4\xbe %s \xe0\xa4\xa1\xe0\xa4\xbe\xe0\xa4\xb2\xe0\xa5\x87\xe0\xa4\x82', -'invalid request': '\xe0\xa4\x85\xe0\xa4\xb5\xe0\xa5\x88\xe0\xa4\xa7 \xe0\xa4\x85\xe0\xa4\xa8\xe0\xa5\x81\xe0\xa4\xb0\xe0\xa5\x8b\xe0\xa4\xa7', -'login': 'login', -'logout': 'logout', -'new record inserted': '\xe0\xa4\xa8\xe0\xa4\xaf\xe0\xa4\xbe \xe0\xa4\xb0\xe0\xa5\x87\xe0\xa4\x95\xe0\xa5\x89\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa1 \xe0\xa4\xa1\xe0\xa4\xbe\xe0\xa4\xb2\xe0\xa4\xbe', -'next 100 rows': '\xe0\xa4\x85\xe0\xa4\x97\xe0\xa4\xb2\xe0\xa5\x87 100 \xe0\xa4\xaa\xe0\xa4\x82\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\x81', -'or import from csv file': '\xe0\xa4\xaf\xe0\xa4\xbe csv \xe0\xa4\xab\xe0\xa4\xbc\xe0\xa4\xbe\xe0\xa4\x87\xe0\xa4\xb2 \xe0\xa4\xb8\xe0\xa5\x87 \xe0\xa4\x86\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\xa4', -'previous 100 rows': '\xe0\xa4\xaa\xe0\xa4\xbf\xe0\xa4\x9b\xe0\xa4\xb2\xe0\xa5\x87 100 \xe0\xa4\xaa\xe0\xa4\x82\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe\xe0\xa4\x81', -'record': 'record', -'record does not exist': '\xe0\xa4\xb0\xe0\xa4\xbf\xe0\xa4\x95\xe0\xa5\x89\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa1 \xe0\xa4\xae\xe0\xa5\x8c\xe0\xa4\x9c\xe0\xa5\x82\xe0\xa4\xa6 \xe0\xa4\xa8\xe0\xa4\xb9\xe0\xa5\x80\xe0\xa4\x82 \xe0\xa4\xb9\xe0\xa5\x88', -'record id': '\xe0\xa4\xb0\xe0\xa4\xbf\xe0\xa4\x95\xe0\xa5\x89\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa1 \xe0\xa4\xaa\xe0\xa4\xb9\xe0\xa4\x9a\xe0\xa4\xbe\xe0\xa4\xa8\xe0\xa4\x95\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa4\xe0\xa4\xbe (\xe0\xa4\x86\xe0\xa4\x88\xe0\xa4\xa1\xe0\xa5\x80)', -'register': 'register', -'selected': '\xe0\xa4\x9a\xe0\xa5\x81\xe0\xa4\xa8\xe0\xa4\xbe \xe0\xa4\xb9\xe0\xa5\x81\xe0\xa4\x86', -'state': '\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\xa5\xe0\xa4\xbf\xe0\xa4\xa4\xe0\xa4\xbf', -'table': '\xe0\xa4\xa4\xe0\xa4\xbe\xe0\xa4\xb2\xe0\xa4\xbf\xe0\xa4\x95\xe0\xa4\xbe', -'unable to parse csv file': 'csv \xe0\xa4\xab\xe0\xa4\xbc\xe0\xa4\xbe\xe0\xa4\x87\xe0\xa4\xb2 \xe0\xa4\xaa\xe0\xa4\xbe\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xb8 \xe0\xa4\x95\xe0\xa4\xb0\xe0\xa4\xa8\xe0\xa5\x87 \xe0\xa4\xae\xe0\xa5\x87\xe0\xa4\x82 \xe0\xa4\x85\xe0\xa4\xb8\xe0\xa4\xae\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa5', -} DELETED applications/welcome/languages/hu-hu.py Index: applications/welcome/languages/hu-hu.py ================================================================== --- applications/welcome/languages/hu-hu.py +++ applications/welcome/languages/hu-hu.py @@ -1,93 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', -'%Y-%m-%d': '%Y.%m.%d.', -'%Y-%m-%d %H:%M:%S': '%Y.%m.%d. %H:%M:%S', -'%s rows deleted': '%s sorok t\xc3\xb6rl\xc5\x91dtek', -'%s rows updated': '%s sorok friss\xc3\xadt\xc5\x91dtek', -'Available databases and tables': 'El\xc3\xa9rhet\xc5\x91 adatb\xc3\xa1zisok \xc3\xa9s t\xc3\xa1bl\xc3\xa1k', -'Cannot be empty': 'Nem lehet \xc3\xbcres', -'Check to delete': 'T\xc3\xb6rl\xc3\xa9shez v\xc3\xa1laszd ki', -'Client IP': 'Client IP', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Current request': 'Jelenlegi lek\xc3\xa9rdez\xc3\xa9s', -'Current response': 'Jelenlegi v\xc3\xa1lasz', -'Current session': 'Jelenlegi folyamat', -'DB Model': 'DB Model', -'Database': 'Adatb\xc3\xa1zis', -'Delete:': 'T\xc3\xb6r\xc3\xb6l:', -'Description': 'Description', -'E-mail': 'E-mail', -'Edit': 'Szerkeszt', -'Edit This App': 'Alkalmaz\xc3\xa1st szerkeszt', -'Edit current record': 'Aktu\xc3\xa1lis bejegyz\xc3\xa9s szerkeszt\xc3\xa9se', -'First name': 'First name', -'Group ID': 'Group ID', -'Hello World': 'Hello Vil\xc3\xa1g', -'Import/Export': 'Import/Export', -'Index': 'Index', -'Internal State': 'Internal State', -'Invalid Query': 'Hib\xc3\xa1s lek\xc3\xa9rdez\xc3\xa9s', -'Invalid email': 'Invalid email', -'Last name': 'Last name', -'Layout': 'Szerkezet', -'Main Menu': 'F\xc5\x91men\xc3\xbc', -'Menu Model': 'Men\xc3\xbc model', -'Name': 'Name', -'New Record': '\xc3\x9aj bejegyz\xc3\xa9s', -'No databases in this application': 'Nincs adatb\xc3\xa1zis ebben az alkalmaz\xc3\xa1sban', -'Origin': 'Origin', -'Password': 'Password', -'Powered by': 'Powered by', -'Query:': 'Lek\xc3\xa9rdez\xc3\xa9s:', -'Record ID': 'Record ID', -'Registration key': 'Registration key', -'Reset Password key': 'Reset Password key', -'Role': 'Role', -'Rows in table': 'Sorok a t\xc3\xa1bl\xc3\xa1ban', -'Rows selected': 'Kiv\xc3\xa1lasztott sorok', -'Stylesheet': 'Stylesheet', -'Sure you want to delete this object?': 'Biztos t\xc3\xb6rli ezt az objektumot?', -'Table name': 'Table name', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', -'Timestamp': 'Timestamp', -'Update:': 'Friss\xc3\xadt:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', -'User ID': 'User ID', -'View': 'N\xc3\xa9zet', -'Welcome %s': 'Welcome %s', -'Welcome to web2py': 'Isten hozott a web2py-ban', -'appadmin is disabled because insecure channel': 'az appadmin a biztons\xc3\xa1gtalan csatorna miatt letiltva', -'cache': 'gyors\xc3\xadt\xc3\xb3t\xc3\xa1r', -'change password': 'jelsz\xc3\xb3 megv\xc3\xa1ltoztat\xc3\xa1sa', -'Online examples': 'online p\xc3\xa9ld\xc3\xa1k\xc3\xa9rt kattints ide', -'Administrative interface': 'az adminisztr\xc3\xa1ci\xc3\xb3s fel\xc3\xbclet\xc3\xa9rt kattints ide', -'customize me!': 'v\xc3\xa1ltoztass meg!', -'data uploaded': 'adat felt\xc3\xb6ltve', -'database': 'adatb\xc3\xa1zis', -'database %s select': 'adatb\xc3\xa1zis %s kiv\xc3\xa1laszt\xc3\xa1s', -'db': 'db', -'design': 'design', -'done!': 'k\xc3\xa9sz!', -'edit profile': 'profil szerkeszt\xc3\xa9se', -'export as csv file': 'export\xc3\xa1l csv f\xc3\xa1jlba', -'insert new': '\xc3\xbaj beilleszt\xc3\xa9se', -'insert new %s': '\xc3\xbaj beilleszt\xc3\xa9se %s', -'invalid request': 'hib\xc3\xa1s k\xc3\xa9r\xc3\xa9s', -'login': 'bel\xc3\xa9p', -'logout': 'kil\xc3\xa9p', -'lost password': 'elveszett jelsz\xc3\xb3', -'new record inserted': '\xc3\xbaj bejegyz\xc3\xa9s felv\xc3\xa9ve', -'next 100 rows': 'k\xc3\xb6vetkez\xc5\x91 100 sor', -'or import from csv file': 'vagy bet\xc3\xb6lt\xc3\xa9s csv f\xc3\xa1jlb\xc3\xb3l', -'previous 100 rows': 'el\xc5\x91z\xc5\x91 100 sor', -'record': 'bejegyz\xc3\xa9s', -'record does not exist': 'bejegyz\xc3\xa9s nem l\xc3\xa9tezik', -'record id': 'bejegyz\xc3\xa9s id', -'register': 'regisztr\xc3\xa1ci\xc3\xb3', -'selected': 'kiv\xc3\xa1lasztott', -'state': '\xc3\xa1llapot', -'table': 't\xc3\xa1bla', -'unable to parse csv file': 'nem lehet a csv f\xc3\xa1jlt beolvasni', -} DELETED applications/welcome/languages/hu.py Index: applications/welcome/languages/hu.py ================================================================== --- applications/welcome/languages/hu.py +++ applications/welcome/languages/hu.py @@ -1,93 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', -'%Y-%m-%d': '%Y.%m.%d.', -'%Y-%m-%d %H:%M:%S': '%Y.%m.%d. %H:%M:%S', -'%s rows deleted': '%s sorok t\xc3\xb6rl\xc5\x91dtek', -'%s rows updated': '%s sorok friss\xc3\xadt\xc5\x91dtek', -'Available databases and tables': 'El\xc3\xa9rhet\xc5\x91 adatb\xc3\xa1zisok \xc3\xa9s t\xc3\xa1bl\xc3\xa1k', -'Cannot be empty': 'Nem lehet \xc3\xbcres', -'Check to delete': 'T\xc3\xb6rl\xc3\xa9shez v\xc3\xa1laszd ki', -'Client IP': 'Client IP', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Current request': 'Jelenlegi lek\xc3\xa9rdez\xc3\xa9s', -'Current response': 'Jelenlegi v\xc3\xa1lasz', -'Current session': 'Jelenlegi folyamat', -'DB Model': 'DB Model', -'Database': 'Adatb\xc3\xa1zis', -'Delete:': 'T\xc3\xb6r\xc3\xb6l:', -'Description': 'Description', -'E-mail': 'E-mail', -'Edit': 'Szerkeszt', -'Edit This App': 'Alkalmaz\xc3\xa1st szerkeszt', -'Edit current record': 'Aktu\xc3\xa1lis bejegyz\xc3\xa9s szerkeszt\xc3\xa9se', -'First name': 'First name', -'Group ID': 'Group ID', -'Hello World': 'Hello Vil\xc3\xa1g', -'Import/Export': 'Import/Export', -'Index': 'Index', -'Internal State': 'Internal State', -'Invalid Query': 'Hib\xc3\xa1s lek\xc3\xa9rdez\xc3\xa9s', -'Invalid email': 'Invalid email', -'Last name': 'Last name', -'Layout': 'Szerkezet', -'Main Menu': 'F\xc5\x91men\xc3\xbc', -'Menu Model': 'Men\xc3\xbc model', -'Name': 'Name', -'New Record': '\xc3\x9aj bejegyz\xc3\xa9s', -'No databases in this application': 'Nincs adatb\xc3\xa1zis ebben az alkalmaz\xc3\xa1sban', -'Origin': 'Origin', -'Password': 'Password', -'Powered by': 'Powered by', -'Query:': 'Lek\xc3\xa9rdez\xc3\xa9s:', -'Record ID': 'Record ID', -'Registration key': 'Registration key', -'Reset Password key': 'Reset Password key', -'Role': 'Role', -'Rows in table': 'Sorok a t\xc3\xa1bl\xc3\xa1ban', -'Rows selected': 'Kiv\xc3\xa1lasztott sorok', -'Stylesheet': 'Stylesheet', -'Sure you want to delete this object?': 'Biztos t\xc3\xb6rli ezt az objektumot?', -'Table name': 'Table name', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', -'Timestamp': 'Timestamp', -'Update:': 'Friss\xc3\xadt:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', -'User ID': 'User ID', -'View': 'N\xc3\xa9zet', -'Welcome %s': 'Welcome %s', -'Welcome to web2py': 'Isten hozott a web2py-ban', -'appadmin is disabled because insecure channel': 'az appadmin a biztons\xc3\xa1gtalan csatorna miatt letiltva', -'cache': 'gyors\xc3\xadt\xc3\xb3t\xc3\xa1r', -'change password': 'jelsz\xc3\xb3 megv\xc3\xa1ltoztat\xc3\xa1sa', -'Online examples': 'online p\xc3\xa9ld\xc3\xa1k\xc3\xa9rt kattints ide', -'Administrative interface': 'az adminisztr\xc3\xa1ci\xc3\xb3s fel\xc3\xbclet\xc3\xa9rt kattints ide', -'customize me!': 'v\xc3\xa1ltoztass meg!', -'data uploaded': 'adat felt\xc3\xb6ltve', -'database': 'adatb\xc3\xa1zis', -'database %s select': 'adatb\xc3\xa1zis %s kiv\xc3\xa1laszt\xc3\xa1s', -'db': 'db', -'design': 'design', -'done!': 'k\xc3\xa9sz!', -'edit profile': 'profil szerkeszt\xc3\xa9se', -'export as csv file': 'export\xc3\xa1l csv f\xc3\xa1jlba', -'insert new': '\xc3\xbaj beilleszt\xc3\xa9se', -'insert new %s': '\xc3\xbaj beilleszt\xc3\xa9se %s', -'invalid request': 'hib\xc3\xa1s k\xc3\xa9r\xc3\xa9s', -'login': 'bel\xc3\xa9p', -'logout': 'kil\xc3\xa9p', -'lost password': 'elveszett jelsz\xc3\xb3', -'new record inserted': '\xc3\xbaj bejegyz\xc3\xa9s felv\xc3\xa9ve', -'next 100 rows': 'k\xc3\xb6vetkez\xc5\x91 100 sor', -'or import from csv file': 'vagy bet\xc3\xb6lt\xc3\xa9s csv f\xc3\xa1jlb\xc3\xb3l', -'previous 100 rows': 'el\xc5\x91z\xc5\x91 100 sor', -'record': 'bejegyz\xc3\xa9s', -'record does not exist': 'bejegyz\xc3\xa9s nem l\xc3\xa9tezik', -'record id': 'bejegyz\xc3\xa9s id', -'register': 'regisztr\xc3\xa1ci\xc3\xb3', -'selected': 'kiv\xc3\xa1lasztott', -'state': '\xc3\xa1llapot', -'table': 't\xc3\xa1bla', -'unable to parse csv file': 'nem lehet a csv f\xc3\xa1jlt beolvasni', -} DELETED applications/welcome/languages/it-it.py Index: applications/welcome/languages/it-it.py ================================================================== --- applications/welcome/languages/it-it.py +++ applications/welcome/languages/it-it.py @@ -1,104 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" è un\'espressione opzionale come "campo1=\'nuovo valore\'". Non si può fare "update" o "delete" dei risultati di un JOIN ', -'%Y-%m-%d': '%d/%m/%Y', -'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', -'%s rows deleted': '%s righe ("record") cancellate', -'%s rows updated': '%s righe ("record") modificate', -'Available databases and tables': 'Database e tabelle disponibili', -'Cannot be empty': 'Non può essere vuoto', -'Check to delete': 'Seleziona per cancellare', -'Client IP': 'Client IP', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Current request': 'Richiesta (request) corrente', -'Current response': 'Risposta (response) corrente', -'Current session': 'Sessione (session) corrente', -'DB Model': 'Modello di DB', -'Database': 'Database', -'Delete:': 'Cancella:', -'Description': 'Descrizione', -'E-mail': 'E-mail', -'Edit': 'Modifica', -'Edit This App': 'Modifica questa applicazione', -'Edit current record': 'Modifica record corrente', -'First name': 'Nome', -'Group ID': 'ID Gruppo', -'Hello World': 'Salve Mondo', -'Hello World in a flash!': 'Salve Mondo in un flash!', -'Import/Export': 'Importa/Esporta', -'Index': 'Indice', -'Internal State': 'Stato interno', -'Invalid Query': 'Richiesta (query) non valida', -'Invalid email': 'Email non valida', -'Last name': 'Cognome', -'Layout': 'Layout', -'Main Menu': 'Menu principale', -'Menu Model': 'Menu Modelli', -'Name': 'Nome', -'New Record': 'Nuovo elemento (record)', -'No databases in this application': 'Nessun database presente in questa applicazione', -'Origin': 'Origine', -'Password': 'Password', -'Powered by': 'Powered by', -'Query:': 'Richiesta (query):', -'Record ID': 'Record ID', -'Registration key': 'Chiave di Registazione', -'Reset Password key': 'Resetta chiave Password ', -'Role': 'Ruolo', -'Rows in table': 'Righe nella tabella', -'Rows selected': 'Righe selezionate', -'Stylesheet': 'Foglio di stile (stylesheet)', -'Sure you want to delete this object?': 'Vuoi veramente cancellare questo oggetto?', -'Table name': 'Nome tabella', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La richiesta (query) è una condizione come ad esempio "db.tabella1.campo1==\'valore\'". Una condizione come "db.tabella1.campo1==db.tabella2.campo2" produce un "JOIN" SQL.', -'The output of the file is a dictionary that was rendered by the view': 'L\'output del file è un "dictionary" che è stato visualizzato dalla vista', -'This is a copy of the scaffolding application': "Questa è una copia dell'applicazione di base (scaffold)", -'Timestamp': 'Ora (timestamp)', -'Update:': 'Aggiorna:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Per costruire richieste (query) più complesse si usano (...)&(...) come "e" (AND), (...)|(...) come "o" (OR), e ~(...) come negazione (NOT).', -'User ID': 'ID Utente', -'View': 'Vista', -'Welcome %s': 'Benvenuto %s', -'Welcome to web2py': 'Benvenuto su web2py', -'Which called the function': 'che ha chiamato la funzione', -'You are successfully running web2py': 'Stai eseguendo web2py con successo', -'You can modify this application and adapt it to your needs': 'Puoi modificare questa applicazione adattandola alle tue necessità', -'You visited the url': "Hai visitato l'URL", -'appadmin is disabled because insecure channel': 'Amministrazione (appadmin) disabilitata: comunicazione non sicura', -'cache': 'cache', -'change password': 'Cambia password', -'Online examples': 'Vedere gli esempi', -'Administrative interface': "Interfaccia amministrativa", -'customize me!': 'Personalizzami!', -'data uploaded': 'dati caricati', -'database': 'database', -'database %s select': 'database %s select', -'db': 'db', -'design': 'progetta', -'Documentation': 'Documentazione', -'done!': 'fatto!', -'edit profile': 'modifica profilo', -'export as csv file': 'esporta come file CSV', -'hello world': 'salve mondo', -'insert new': 'inserisci nuovo', -'insert new %s': 'inserisci nuovo %s', -'invalid request': 'richiesta non valida', -'located in the file': 'presente nel file', -'login': 'accesso', -'logout': 'uscita', -'lost password?': 'dimenticato la password?', -'new record inserted': 'nuovo record inserito', -'next 100 rows': 'prossime 100 righe', -'not authorized': 'non autorizzato', -'or import from csv file': 'oppure importa da file CSV', -'previous 100 rows': '100 righe precedenti', -'record': 'record', -'record does not exist': 'il record non esiste', -'record id': 'record id', -'register': 'registrazione', -'selected': 'selezionato', -'state': 'stato', -'table': 'tabella', -'unable to parse csv file': 'non riesco a decodificare questo file CSV', -} DELETED applications/welcome/languages/it.py Index: applications/welcome/languages/it.py ================================================================== --- applications/welcome/languages/it.py +++ applications/welcome/languages/it.py @@ -1,104 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" è un\'espressione opzionale come "campo1=\'nuovo valore\'". Non si può fare "update" o "delete" dei risultati di un JOIN ', -'%Y-%m-%d': '%d/%m/%Y', -'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', -'%s rows deleted': '%s righe ("record") cancellate', -'%s rows updated': '%s righe ("record") modificate', -'Available databases and tables': 'Database e tabelle disponibili', -'Cannot be empty': 'Non può essere vuoto', -'Check to delete': 'Seleziona per cancellare', -'Client IP': 'Client IP', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Current request': 'Richiesta (request) corrente', -'Current response': 'Risposta (response) corrente', -'Current session': 'Sessione (session) corrente', -'DB Model': 'Modello di DB', -'Database': 'Database', -'Delete:': 'Cancella:', -'Description': 'Descrizione', -'E-mail': 'E-mail', -'Edit': 'Modifica', -'Edit This App': 'Modifica questa applicazione', -'Edit current record': 'Modifica record corrente', -'First name': 'Nome', -'Group ID': 'ID Gruppo', -'Hello World': 'Salve Mondo', -'Hello World in a flash!': 'Salve Mondo in un flash!', -'Import/Export': 'Importa/Esporta', -'Index': 'Indice', -'Internal State': 'Stato interno', -'Invalid Query': 'Richiesta (query) non valida', -'Invalid email': 'Email non valida', -'Last name': 'Cognome', -'Layout': 'Layout', -'Main Menu': 'Menu principale', -'Menu Model': 'Menu Modelli', -'Name': 'Nome', -'New Record': 'Nuovo elemento (record)', -'No databases in this application': 'Nessun database presente in questa applicazione', -'Origin': 'Origine', -'Password': 'Password', -'Powered by': 'Powered by', -'Query:': 'Richiesta (query):', -'Record ID': 'Record ID', -'Registration key': 'Chiave di Registazione', -'Reset Password key': 'Resetta chiave Password ', -'Role': 'Ruolo', -'Rows in table': 'Righe nella tabella', -'Rows selected': 'Righe selezionate', -'Stylesheet': 'Foglio di stile (stylesheet)', -'Sure you want to delete this object?': 'Vuoi veramente cancellare questo oggetto?', -'Table name': 'Nome tabella', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La richiesta (query) è una condizione come ad esempio "db.tabella1.campo1==\'valore\'". Una condizione come "db.tabella1.campo1==db.tabella2.campo2" produce un "JOIN" SQL.', -'The output of the file is a dictionary that was rendered by the view': 'L\'output del file è un "dictionary" che è stato visualizzato dalla vista', -'This is a copy of the scaffolding application': "Questa è una copia dell'applicazione di base (scaffold)", -'Timestamp': 'Ora (timestamp)', -'Update:': 'Aggiorna:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Per costruire richieste (query) più complesse si usano (...)&(...) come "e" (AND), (...)|(...) come "o" (OR), e ~(...) come negazione (NOT).', -'User ID': 'ID Utente', -'View': 'Vista', -'Welcome %s': 'Benvenuto %s', -'Welcome to web2py': 'Benvenuto su web2py', -'Which called the function': 'che ha chiamato la funzione', -'You are successfully running web2py': 'Stai eseguendo web2py con successo', -'You can modify this application and adapt it to your needs': 'Puoi modificare questa applicazione adattandola alle tue necessità', -'You visited the url': "Hai visitato l'URL", -'appadmin is disabled because insecure channel': 'Amministrazione (appadmin) disabilitata: comunicazione non sicura', -'cache': 'cache', -'change password': 'Cambia password', -'Online examples': 'Vedere gli esempi', -'Administrative interface': "Interfaccia amministrativa", -'customize me!': 'Personalizzami!', -'data uploaded': 'dati caricati', -'database': 'database', -'database %s select': 'database %s select', -'db': 'db', -'design': 'progetta', -'Documentation': 'Documentazione', -'done!': 'fatto!', -'edit profile': 'modifica profilo', -'export as csv file': 'esporta come file CSV', -'hello world': 'salve mondo', -'insert new': 'inserisci nuovo', -'insert new %s': 'inserisci nuovo %s', -'invalid request': 'richiesta non valida', -'located in the file': 'presente nel file', -'login': 'accesso', -'logout': 'uscita', -'lost password?': 'dimenticato la password?', -'new record inserted': 'nuovo record inserito', -'next 100 rows': 'prossime 100 righe', -'not authorized': 'non autorizzato', -'or import from csv file': 'oppure importa da file CSV', -'previous 100 rows': '100 righe precedenti', -'record': 'record', -'record does not exist': 'il record non esiste', -'record id': 'record id', -'register': 'registrazione', -'selected': 'selezionato', -'state': 'stato', -'table': 'tabella', -'unable to parse csv file': 'non riesco a decodificare questo file CSV', -} DELETED applications/welcome/languages/pl-pl.py Index: applications/welcome/languages/pl-pl.py ================================================================== --- applications/welcome/languages/pl-pl.py +++ applications/welcome/languages/pl-pl.py @@ -1,81 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Uaktualnij" jest dodatkowym wyra\xc5\xbceniem postaci "pole1=\'nowawarto\xc5\x9b\xc4\x87\'". Nie mo\xc5\xbcesz uaktualni\xc4\x87 lub usun\xc4\x85\xc4\x87 wynik\xc3\xb3w z JOIN:', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': 'Wierszy usuni\xc4\x99tych: %s', -'%s rows updated': 'Wierszy uaktualnionych: %s', -'Available databases and tables': 'Dost\xc4\x99pne bazy danych i tabele', -'Cannot be empty': 'Nie mo\xc5\xbce by\xc4\x87 puste', -'Change Password': 'Change Password', -'Check to delete': 'Zaznacz aby usun\xc4\x85\xc4\x87', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Current request': 'Aktualne \xc5\xbc\xc4\x85danie', -'Current response': 'Aktualna odpowied\xc5\xba', -'Current session': 'Aktualna sesja', -'DB Model': 'DB Model', -'Database': 'Database', -'Delete:': 'Usu\xc5\x84:', -'Edit': 'Edit', -'Edit Profile': 'Edit Profile', -'Edit This App': 'Edit This App', -'Edit current record': 'Edytuj aktualny rekord', -'Hello World': 'Witaj \xc5\x9awiecie', -'Import/Export': 'Importuj/eksportuj', -'Index': 'Index', -'Internal State': 'Stan wewn\xc4\x99trzny', -'Invalid Query': 'B\xc5\x82\xc4\x99dne zapytanie', -'Layout': 'Layout', -'Login': 'Zaloguj', -'Logout': 'Logout', -'Lost Password': 'Przypomnij has\xc5\x82o', -'Main Menu': 'Main Menu', -'Menu Model': 'Menu Model', -'New Record': 'Nowy rekord', -'No databases in this application': 'Brak baz danych w tej aplikacji', -'Powered by': 'Powered by', -'Query:': 'Zapytanie:', -'Register': 'Zarejestruj', -'Rows in table': 'Wiersze w tabeli', -'Rows selected': 'Wybrane wiersze', -'Stylesheet': 'Stylesheet', -'Sure you want to delete this object?': 'Czy na pewno chcesz usun\xc4\x85\xc4\x87 ten obiekt?', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Zapytanie" jest warunkiem postaci "db.tabela1.pole1==\'warto\xc5\x9b\xc4\x87\'". Takie co\xc5\x9b jak "db.tabela1.pole1==db.tabela2.pole2" oznacza SQL JOIN.', -'Update:': 'Uaktualnij:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'U\xc5\xbcyj (...)&(...) jako AND, (...)|(...) jako OR oraz ~(...) jako NOT do tworzenia bardziej skomplikowanych zapyta\xc5\x84.', -'View': 'View', -'Welcome %s': 'Welcome %s', -'Welcome to web2py': 'Witaj w web2py', -'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', -'cache': 'cache', -'change password': 'change password', -'Online examples': 'Kliknij aby przej\xc5\x9b\xc4\x87 do interaktywnych przyk\xc5\x82ad\xc3\xb3w', -'Administrative interface': 'Kliknij aby przej\xc5\x9b\xc4\x87 do panelu administracyjnego', -'customize me!': 'dostosuj mnie!', -'data uploaded': 'dane wys\xc5\x82ane', -'database': 'baza danych', -'database %s select': 'wyb\xc3\xb3r z bazy danych %s', -'db': 'baza danych', -'design': 'projektuj', -'done!': 'zrobione!', -'edit profile': 'edit profile', -'export as csv file': 'eksportuj jako plik csv', -'insert new': 'wstaw nowy rekord tabeli', -'insert new %s': 'wstaw nowy rekord do tabeli %s', -'invalid request': 'B\xc5\x82\xc4\x99dne \xc5\xbc\xc4\x85danie', -'login': 'login', -'logout': 'logout', -'new record inserted': 'nowy rekord zosta\xc5\x82 wstawiony', -'next 100 rows': 'nast\xc4\x99pne 100 wierszy', -'or import from csv file': 'lub zaimportuj z pliku csv', -'previous 100 rows': 'poprzednie 100 wierszy', -'record': 'record', -'record does not exist': 'rekord nie istnieje', -'record id': 'id rekordu', -'register': 'register', -'selected': 'wybranych', -'state': 'stan', -'table': 'tabela', -'unable to parse csv file': 'nie mo\xc5\xbcna sparsowa\xc4\x87 pliku csv', -} DELETED applications/welcome/languages/pl.py Index: applications/welcome/languages/pl.py ================================================================== --- applications/welcome/languages/pl.py +++ applications/welcome/languages/pl.py @@ -1,104 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Uaktualnij" jest dodatkowym wyra\xc5\xbceniem postaci "pole1=\'nowawarto\xc5\x9b\xc4\x87\'". Nie mo\xc5\xbcesz uaktualni\xc4\x87 lub usun\xc4\x85\xc4\x87 wynik\xc3\xb3w z JOIN:', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': 'Wierszy usuni\xc4\x99tych: %s', -'%s rows updated': 'Wierszy uaktualnionych: %s', -'Authentication': 'Uwierzytelnienie', -'Available databases and tables': 'Dost\xc4\x99pne bazy danych i tabele', -'Cannot be empty': 'Nie mo\xc5\xbce by\xc4\x87 puste', -'Change Password': 'Zmie\xc5\x84 has\xc5\x82o', -'Check to delete': 'Zaznacz aby usun\xc4\x85\xc4\x87', -'Check to delete:': 'Zaznacz aby usun\xc4\x85\xc4\x87:', -'Client IP': 'IP klienta', -'Controller': 'Kontroler', -'Copyright': 'Copyright', -'Current request': 'Aktualne \xc5\xbc\xc4\x85danie', -'Current response': 'Aktualna odpowied\xc5\xba', -'Current session': 'Aktualna sesja', -'DB Model': 'Model bazy danych', -'Database': 'Baza danych', -'Delete:': 'Usu\xc5\x84:', -'Description': 'Opis', -'E-mail': 'Adres e-mail', -'Edit': 'Edycja', -'Edit Profile': 'Edytuj profil', -'Edit This App': 'Edytuj t\xc4\x99 aplikacj\xc4\x99', -'Edit current record': 'Edytuj obecny rekord', -'First name': 'Imi\xc4\x99', -'Function disabled': 'Funkcja wy\xc5\x82\xc4\x85czona', -'Group ID': 'ID grupy', -'Hello World': 'Witaj \xc5\x9awiecie', -'Import/Export': 'Importuj/eksportuj', -'Index': 'Indeks', -'Internal State': 'Stan wewn\xc4\x99trzny', -'Invalid Query': 'B\xc5\x82\xc4\x99dne zapytanie', -'Invalid email': 'B\xc5\x82\xc4\x99dny adres email', -'Last name': 'Nazwisko', -'Layout': 'Uk\xc5\x82ad', -'Login': 'Zaloguj', -'Logout': 'Wyloguj', -'Lost Password': 'Przypomnij has\xc5\x82o', -'Main Menu': 'Menu g\xc5\x82\xc3\xb3wne', -'Menu Model': 'Model menu', -'Name': 'Nazwa', -'New Record': 'Nowy rekord', -'No databases in this application': 'Brak baz danych w tej aplikacji', -'Origin': '\xc5\xb9r\xc3\xb3d\xc5\x82o', -'Password': 'Has\xc5\x82o', -"Password fields don't match": 'Pola has\xc5\x82a nie s\xc4\x85 zgodne ze sob\xc4\x85', -'Powered by': 'Zasilane przez', -'Query:': 'Zapytanie:', -'Record ID': 'ID rekordu', -'Register': 'Zarejestruj', -'Registration key': 'Klucz rejestracji', -'Role': 'Rola', -'Rows in table': 'Wiersze w tabeli', -'Rows selected': 'Wybrane wiersze', -'Stylesheet': 'Arkusz styl\xc3\xb3w', -'Submit': 'Wy\xc5\x9blij', -'Sure you want to delete this object?': 'Czy na pewno chcesz usun\xc4\x85\xc4\x87 ten obiekt?', -'Table name': 'Nazwa tabeli', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Zapytanie" jest warunkiem postaci "db.tabela1.pole1==\'warto\xc5\x9b\xc4\x87\'". Takie co\xc5\x9b jak "db.tabela1.pole1==db.tabela2.pole2" oznacza SQL JOIN.', -'Timestamp': 'Znacznik czasu', -'Update:': 'Uaktualnij:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'U\xc5\xbcyj (...)&(...) jako AND, (...)|(...) jako OR oraz ~(...) jako NOT do tworzenia bardziej skomplikowanych zapyta\xc5\x84.', -'User %(id)s Registered': 'U\xc5\xbcytkownik %(id)s zosta\xc5\x82 zarejestrowany', -'User ID': 'ID u\xc5\xbcytkownika', -'Verify Password': 'Potwierd\xc5\xba has\xc5\x82o', -'View': 'Widok', -'Welcome %s': 'Welcome %s', -'Welcome to web2py': 'Witaj w web2py', -'appadmin is disabled because insecure channel': 'administracja aplikacji wy\xc5\x82\xc4\x85czona z powodu braku bezpiecznego po\xc5\x82\xc4\x85czenia', -'cache': 'cache', -'change password': 'change password', -'Online examples': 'Kliknij aby przej\xc5\x9b\xc4\x87 do interaktywnych przyk\xc5\x82ad\xc3\xb3w', -'Administrative interface': 'Kliknij aby przej\xc5\x9b\xc4\x87 do panelu administracyjnego', -'customize me!': 'dostosuj mnie!', -'data uploaded': 'dane wys\xc5\x82ane', -'database': 'baza danych', -'database %s select': 'wyb\xc3\xb3r z bazy danych %s', -'db': 'baza danych', -'design': 'projektuj', -'done!': 'zrobione!', -'edit profile': 'edit profile', -'export as csv file': 'eksportuj jako plik csv', -'insert new': 'wstaw nowy rekord tabeli', -'insert new %s': 'wstaw nowy rekord do tabeli %s', -'invalid request': 'B\xc5\x82\xc4\x99dne \xc5\xbc\xc4\x85danie', -'login': 'login', -'logout': 'logout', -'new record inserted': 'nowy rekord zosta\xc5\x82 wstawiony', -'next 100 rows': 'nast\xc4\x99pne 100 wierszy', -'or import from csv file': 'lub zaimportuj z pliku csv', -'previous 100 rows': 'poprzednie 100 wierszy', -'record': 'rekord', -'record does not exist': 'rekord nie istnieje', -'record id': 'id rekordu', -'register': 'register', -'selected': 'wybranych', -'state': 'stan', -'table': 'tabela', -'unable to parse csv file': 'nie mo\xc5\xbcna sparsowa\xc4\x87 pliku csv', -} DELETED applications/welcome/languages/pt-br.py Index: applications/welcome/languages/pt-br.py ================================================================== --- applications/welcome/languages/pt-br.py +++ applications/welcome/languages/pt-br.py @@ -1,142 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" é uma expressão opcional como "campo1=\'novovalor\'". Você não pode atualizar ou apagar os resultados de um JOIN', -'%Y-%m-%d': '%d-%m-%Y', -'%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', -'%s rows deleted': '%s linhas apagadas', -'%s rows updated': '%s linhas atualizadas', -'About': 'About', -'Access Control': 'Access Control', -'Ajax Recipes': 'Ajax Recipes', -'Available databases and tables': 'Bancos de dados e tabelas disponíveis', -'Buy this book': 'Buy this book', -'Cannot be empty': 'Não pode ser vazio', -'Check to delete': 'Marque para apagar', -'Client IP': 'Client IP', -'Community': 'Community', -'Controller': 'Controlador', -'Copyright': 'Copyright', -'Current request': 'Requisição atual', -'Current response': 'Resposta atual', -'Current session': 'Sessão atual', -'DB Model': 'Modelo BD', -'Database': 'Banco de dados', -'Delete:': 'Apagar:', -'Demo': 'Demo', -'Deployment Recipes': 'Deployment Recipes', -'Description': 'Description', -'Documentation': 'Documentation', -'Download': 'Download', -'E-mail': 'E-mail', -'Edit': 'Editar', -'Edit This App': 'Edit This App', -'Edit current record': 'Editar o registro atual', -'Errors': 'Errors', -'FAQ': 'FAQ', -'First name': 'First name', -'Forms and Validators': 'Forms and Validators', -'Free Applications': 'Free Applications', -'Group ID': 'Group ID', -'Groups': 'Groups', -'Hello World': 'Olá Mundo', -'Home': 'Home', -'Import/Export': 'Importar/Exportar', -'Index': 'Início', -'Internal State': 'Estado Interno', -'Introduction': 'Introduction', -'Invalid Query': 'Consulta Inválida', -'Invalid email': 'Invalid email', -'Last name': 'Last name', -'Layout': 'Layout', -'Layouts': 'Layouts', -'Live chat': 'Live chat', -'Login': 'Autentique-se', -'Lost Password': 'Esqueceu sua senha?', -'Main Menu': 'Menu Principal', -'Menu Model': 'Modelo de Menu', -'Name': 'Name', -'New Record': 'Novo Registro', -'No databases in this application': 'Sem bancos de dados nesta aplicação', -'Origin': 'Origin', -'Other Recipes': 'Other Recipes', -'Overview': 'Overview', -'Password': 'Password', -'Plugins': 'Plugins', -'Powered by': 'Powered by', -'Preface': 'Preface', -'Python': 'Python', -'Query:': 'Consulta:', -'Quick Examples': 'Quick Examples', -'Recipes': 'Recipes', -'Record ID': 'Record ID', -'Register': 'Registre-se', -'Registration key': 'Registration key', -'Reset Password key': 'Reset Password key', -'Resources': 'Resources', -'Role': 'Role', -'Rows in table': 'Linhas na tabela', -'Rows selected': 'Linhas selecionadas', -'Semantic': 'Semantic', -'Services': 'Services', -'Stylesheet': 'Stylesheet', -'Support': 'Support', -'Sure you want to delete this object?': 'Está certo(a) que deseja apagar esse objeto ?', -'Table name': 'Table name', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'Uma "consulta" é uma condição como "db.tabela1.campo1==\'valor\'". Expressões como "db.tabela1.campo1==db.tabela2.campo2" resultam em um JOIN SQL.', -'The Core': 'The Core', -'The Views': 'The Views', -'The output of the file is a dictionary that was rendered by the view': 'The output of the file is a dictionary that was rendered by the view', -'This App': 'This App', -'This is a copy of the scaffolding application': 'This is a copy of the scaffolding application', -'Timestamp': 'Timestamp', -'Twitter': 'Twitter', -'Update:': 'Atualizar:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, e ~(...) para NOT para construir consultas mais complexas.', -'User ID': 'User ID', -'User Voice': 'User Voice', -'Videos': 'Videos', -'View': 'Visualização', -'Web2py': 'Web2py', -'Welcome': 'Welcome', -'Welcome %s': 'Vem vindo %s', -'Welcome to web2py': 'Bem vindo ao web2py', -'Which called the function': 'Which called the function', -'You are successfully running web2py': 'You are successfully running web2py', -'You are successfully running web2py.': 'You are successfully running web2py.', -'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', -'You visited the url': 'You visited the url', -'appadmin is disabled because insecure channel': 'Administração desativada devido ao canal inseguro', -'cache': 'cache', -'change password': 'modificar senha', -'Online examples': 'Alguns exemplos', -'Administrative interface': 'Interface administrativa', -'customize me!': 'Personalize-me!', -'data uploaded': 'dados enviados', -'database': 'banco de dados', -'database %s select': 'Selecionar banco de dados %s', -'db': 'bd', -'design': 'design', -'Documentation': 'Documentation', -'done!': 'concluído!', -'edit profile': 'editar perfil', -'export as csv file': 'exportar como um arquivo csv', -'insert new': 'inserir novo', -'insert new %s': 'inserir novo %s', -'invalid request': 'requisição inválida', -'located in the file': 'located in the file', -'login': 'Entrar', -'logout': 'Sair', -'lost password?': 'lost password?', -'new record inserted': 'novo registro inserido', -'next 100 rows': 'próximas 100 linhas', -'or import from csv file': 'ou importar de um arquivo csv', -'previous 100 rows': '100 linhas anteriores', -'record': 'registro', -'record does not exist': 'registro não existe', -'record id': 'id do registro', -'register': 'Registre-se', -'selected': 'selecionado', -'state': 'estado', -'table': 'tabela', -'unable to parse csv file': 'não foi possível analisar arquivo csv', -} DELETED applications/welcome/languages/pt-pt.py Index: applications/welcome/languages/pt-pt.py ================================================================== --- applications/welcome/languages/pt-pt.py +++ applications/welcome/languages/pt-pt.py @@ -1,116 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" é uma expressão opcional como "field1=\'newvalue\'". Não pode actualizar ou eliminar os resultados de um JOIN', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '%s linhas eliminadas', -'%s rows updated': '%s linhas actualizadas', -'About': 'About', -'Author Reference Auth User': 'Author Reference Auth User', -'Author Reference Auth User.username': 'Author Reference Auth User.username', -'Available databases and tables': 'bases de dados e tabelas disponíveis', -'Cannot be empty': 'não pode ser vazio', -'Category Create': 'Category Create', -'Category Select': 'Category Select', -'Check to delete': 'seleccione para eliminar', -'Comment Create': 'Comment Create', -'Comment Select': 'Comment Select', -'Content': 'Content', -'Controller': 'Controlador', -'Copyright': 'Direitos de cópia', -'Created By': 'Created By', -'Created On': 'Created On', -'Current request': 'pedido currente', -'Current response': 'resposta currente', -'Current session': 'sessão currente', -'DB Model': 'Modelo de BD', -'Database': 'Base de dados', -'Delete:': 'Eliminar:', -'Edit': 'Editar', -'Edit This App': 'Edite esta aplicação', -'Edit current record': 'Edição de registo currente', -'Email': 'Email', -'First Name': 'First Name', -'For %s #%s': 'For %s #%s', -'Hello World': 'Olá Mundo', -'Import/Export': 'Importar/Exportar', -'Index': 'Índice', -'Internal State': 'Estado interno', -'Invalid Query': 'Consulta Inválida', -'Last Name': 'Last Name', -'Layout': 'Esboço', -'Main Menu': 'Menu Principal', -'Menu Model': 'Menu do Modelo', -'Modified By': 'Modified By', -'Modified On': 'Modified On', -'Name': 'Name', -'New Record': 'Novo Registo', -'No Data': 'No Data', -'No databases in this application': 'Não há bases de dados nesta aplicação', -'Password': 'Password', -'Post Create': 'Post Create', -'Post Select': 'Post Select', -'Powered by': 'Suportado por', -'Query:': 'Interrogação:', -'Replyto Reference Post': 'Replyto Reference Post', -'Rows in table': 'Linhas numa tabela', -'Rows selected': 'Linhas seleccionadas', -'Stylesheet': 'Folha de estilo', -'Sure you want to delete this object?': 'Tem a certeza que deseja eliminar este objecto?', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'A "query" é uma condição do tipo "db.table1.field1==\'value\'". Algo como "db.table1.field1==db.table2.field2" resultaria num SQL JOIN.', -'Title': 'Title', -'Update:': 'Actualização:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Utilize (...)&(...) para AND, (...)|(...) para OR, e ~(...) para NOT para construir interrogações mais complexas.', -'Username': 'Username', -'View': 'Vista', -'Welcome %s': 'Bem-vindo(a) %s', -'Welcome to Gluonization': 'Bem vindo ao Web2py', -'Welcome to web2py': 'Bem-vindo(a) ao web2py', -'When': 'When', -'appadmin is disabled because insecure channel': 'appadmin está desactivada pois o canal é inseguro', -'cache': 'cache', -'change password': 'alterar palavra-chave', -'Online examples': 'Exemplos online', -'Administrative interface': 'Painel administrativo', -'create new category': 'create new category', -'create new comment': 'create new comment', -'create new post': 'create new post', -'customize me!': 'Personaliza-me!', -'data uploaded': 'informação enviada', -'database': 'base de dados', -'database %s select': 'selecção de base de dados %s', -'db': 'bd', -'design': 'design', -'done!': 'concluído!', -'edit category': 'edit category', -'edit comment': 'edit comment', -'edit post': 'edit post', -'edit profile': 'Editar perfil', -'export as csv file': 'exportar como ficheiro csv', -'insert new': 'inserir novo', -'insert new %s': 'inserir novo %s', -'invalid request': 'Pedido Inválido', -'login': 'login', -'logout': 'logout', -'new record inserted': 'novo registo inserido', -'next 100 rows': 'próximas 100 linhas', -'or import from csv file': 'ou importe a partir de ficheiro csv', -'previous 100 rows': '100 linhas anteriores', -'record': 'registo', -'record does not exist': 'registo inexistente', -'record id': 'id de registo', -'register': 'register', -'search category': 'search category', -'search comment': 'search comment', -'search post': 'search post', -'select category': 'select category', -'select comment': 'select comment', -'select post': 'select post', -'selected': 'seleccionado(s)', -'show category': 'show category', -'show comment': 'show comment', -'show post': 'show post', -'state': 'estado', -'table': 'tabela', -'unable to parse csv file': 'não foi possível carregar ficheiro csv', -} DELETED applications/welcome/languages/pt.py Index: applications/welcome/languages/pt.py ================================================================== --- applications/welcome/languages/pt.py +++ applications/welcome/languages/pt.py @@ -1,116 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" é uma expressão opcional como "field1=\'newvalue\'". Não pode actualizar ou eliminar os resultados de um JOIN', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '%s linhas eliminadas', -'%s rows updated': '%s linhas actualizadas', -'About': 'About', -'Author Reference Auth User': 'Author Reference Auth User', -'Author Reference Auth User.username': 'Author Reference Auth User.username', -'Available databases and tables': 'bases de dados e tabelas disponíveis', -'Cannot be empty': 'não pode ser vazio', -'Category Create': 'Category Create', -'Category Select': 'Category Select', -'Check to delete': 'seleccione para eliminar', -'Comment Create': 'Comment Create', -'Comment Select': 'Comment Select', -'Content': 'Content', -'Controller': 'Controlador', -'Copyright': 'Direitos de cópia', -'Created By': 'Created By', -'Created On': 'Created On', -'Current request': 'pedido currente', -'Current response': 'resposta currente', -'Current session': 'sessão currente', -'DB Model': 'Modelo de BD', -'Database': 'Base de dados', -'Delete:': 'Eliminar:', -'Edit': 'Editar', -'Edit This App': 'Edite esta aplicação', -'Edit current record': 'Edição de registo currente', -'Email': 'Email', -'First Name': 'First Name', -'For %s #%s': 'For %s #%s', -'Hello World': 'Olá Mundo', -'Import/Export': 'Importar/Exportar', -'Index': 'Índice', -'Internal State': 'Estado interno', -'Invalid Query': 'Consulta Inválida', -'Last Name': 'Last Name', -'Layout': 'Esboço', -'Main Menu': 'Menu Principal', -'Menu Model': 'Menu do Modelo', -'Modified By': 'Modified By', -'Modified On': 'Modified On', -'Name': 'Name', -'New Record': 'Novo Registo', -'No Data': 'No Data', -'No databases in this application': 'Não há bases de dados nesta aplicação', -'Password': 'Password', -'Post Create': 'Post Create', -'Post Select': 'Post Select', -'Powered by': 'Suportado por', -'Query:': 'Interrogação:', -'Replyto Reference Post': 'Replyto Reference Post', -'Rows in table': 'Linhas numa tabela', -'Rows selected': 'Linhas seleccionadas', -'Stylesheet': 'Folha de estilo', -'Sure you want to delete this object?': 'Tem a certeza que deseja eliminar este objecto?', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'A "query" é uma condição do tipo "db.table1.field1==\'value\'". Algo como "db.table1.field1==db.table2.field2" resultaria num SQL JOIN.', -'Title': 'Title', -'Update:': 'Actualização:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Utilize (...)&(...) para AND, (...)|(...) para OR, e ~(...) para NOT para construir interrogações mais complexas.', -'Username': 'Username', -'View': 'Vista', -'Welcome %s': 'Bem-vindo(a) %s', -'Welcome to Gluonization': 'Bem vindo ao Web2py', -'Welcome to web2py': 'Bem-vindo(a) ao web2py', -'When': 'When', -'appadmin is disabled because insecure channel': 'appadmin está desactivada pois o canal é inseguro', -'cache': 'cache', -'change password': 'alterar palavra-chave', -'Online examples': 'Exemplos online', -'Administrative interface': 'Painel administrativo', -'create new category': 'create new category', -'create new comment': 'create new comment', -'create new post': 'create new post', -'customize me!': 'Personaliza-me!', -'data uploaded': 'informação enviada', -'database': 'base de dados', -'database %s select': 'selecção de base de dados %s', -'db': 'bd', -'design': 'design', -'done!': 'concluído!', -'edit category': 'edit category', -'edit comment': 'edit comment', -'edit post': 'edit post', -'edit profile': 'Editar perfil', -'export as csv file': 'exportar como ficheiro csv', -'insert new': 'inserir novo', -'insert new %s': 'inserir novo %s', -'invalid request': 'Pedido Inválido', -'login': 'login', -'logout': 'logout', -'new record inserted': 'novo registo inserido', -'next 100 rows': 'próximas 100 linhas', -'or import from csv file': 'ou importe a partir de ficheiro csv', -'previous 100 rows': '100 linhas anteriores', -'record': 'registo', -'record does not exist': 'registo inexistente', -'record id': 'id de registo', -'register': 'register', -'search category': 'search category', -'search comment': 'search comment', -'search post': 'search post', -'select category': 'select category', -'select comment': 'select comment', -'select post': 'select post', -'selected': 'seleccionado(s)', -'show category': 'show category', -'show comment': 'show comment', -'show post': 'show post', -'state': 'estado', -'table': 'tabela', -'unable to parse csv file': 'não foi possível carregar ficheiro csv', -} DELETED applications/welcome/languages/ru-ru.py Index: applications/welcome/languages/ru-ru.py ================================================================== --- applications/welcome/languages/ru-ru.py +++ applications/welcome/languages/ru-ru.py @@ -1,96 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Изменить" - необязательное выражение вида "field1=\'новое значение\'". Результаты операции JOIN нельзя изменить или удалить.', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '%s строк удалено', -'%s rows updated': '%s строк изменено', -'Available databases and tables': 'Базы данных и таблицы', -'Cannot be empty': 'Пустое значение недопустимо', -'Change Password': 'Смените пароль', -'Check to delete': 'Удалить', -'Check to delete:': 'Удалить:', -'Client IP': 'Client IP', -'Current request': 'Текущий запрос', -'Current response': 'Текущий ответ', -'Current session': 'Текущая сессия', -'Delete:': 'Удалить:', -'Description': 'Описание', -'E-mail': 'E-mail', -'Edit Profile': 'Редактировать профиль', -'Edit current record': 'Редактировать текущую запись', -'First name': 'Имя', -'Group ID': 'Group ID', -'Hello World': 'Заработало!', -'Import/Export': 'Импорт/экспорт', -'Internal State': 'Внутренне состояние', -'Invalid Query': 'Неверный запрос', -'Invalid email': 'Неверный email', -'Invalid login': 'Неверный логин', -'Invalid password': 'Неверный пароль', -'Last name': 'Фамилия', -'Logged in': 'Вход выполнен', -'Logged out': 'Выход выполнен', -'Login': 'Вход', -'Logout': 'Выход', -'Lost Password': 'Забыли пароль?', -'Name': 'Name', -'New Record': 'Новая запись', -'New password': 'Новый пароль', -'No databases in this application': 'В приложении нет баз данных', -'Old password': 'Старый пароль', -'Origin': 'Происхождение', -'Password': 'Пароль', -"Password fields don't match": 'Пароли не совпадают', -'Query:': 'Запрос:', -'Record ID': 'ID записи', -'Register': 'Зарегистрироваться', -'Registration key': 'Ключ регистрации', -'Remember me (for 30 days)': 'Запомнить меня (на 30 дней)', -'Reset Password key': 'Сбросить ключ пароля', -'Role': 'Роль', -'Rows in table': 'Строк в таблице', -'Rows selected': 'Выделено строк', -'Submit': 'Отправить', -'Sure you want to delete this object?': 'Подтвердите удаление объекта', -'Table name': 'Имя таблицы', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Запрос" - это условие вида "db.table1.field1==\'значение\'". Выражение вида "db.table1.field1==db.table2.field2" формирует SQL JOIN.', -'Timestamp': 'Отметка времени', -'Update:': 'Изменить:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Для построение сложных запросов используйте операторы "И": (...)&(...), "ИЛИ": (...)|(...), "НЕ": ~(...).', -'User %(id)s Logged-in': 'Пользователь %(id)s вошёл', -'User %(id)s Logged-out': 'Пользователь %(id)s вышел', -'User %(id)s Password changed': 'Пользователь %(id)s сменил пароль', -'User %(id)s Profile updated': 'Пользователь %(id)s обновил профиль', -'User %(id)s Registered': 'Пользователь %(id)s зарегистрировался', -'User ID': 'ID пользователя', -'Verify Password': 'Повторите пароль', -'Welcome to web2py': 'Добро пожаловать в web2py', -'Online examples': 'примеры он-лайн', -'Administrative interface': 'административный интерфейс', -'customize me!': 'настройте внешний вид!', -'data uploaded': 'данные загружены', -'database': 'база данных', -'database %s select': 'выбор базы данных %s', -'db': 'БД', -'design': 'дизайн', -'done!': 'готово!', -'export as csv file': 'экспорт в csv-файл', -'insert new': 'добавить', -'insert new %s': 'добавить %s', -'invalid request': 'неверный запрос', -'login': 'вход', -'logout': 'выход', -'new record inserted': 'новая запись добавлена', -'next 100 rows': 'следующие 100 строк', -'or import from csv file': 'или импорт из csv-файла', -'password': 'пароль', -'previous 100 rows': 'предыдущие 100 строк', -'profile': 'профиль', -'record does not exist': 'запись не найдена', -'record id': 'id записи', -'selected': 'выбрано', -'state': 'состояние', -'table': 'таблица', -'unable to parse csv file': 'нечитаемый csv-файл', -} DELETED applications/welcome/languages/sk-sk.py Index: applications/welcome/languages/sk-sk.py ================================================================== --- applications/welcome/languages/sk-sk.py +++ applications/welcome/languages/sk-sk.py @@ -1,111 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" je voliteľný výraz ako "field1=\'newvalue\'". Nemôžete upravovať alebo zmazať výsledky JOINu', -'%Y-%m-%d': '%d.%m.%Y', -'%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', -'%s rows deleted': '%s zmazaných záznamov', -'%s rows updated': '%s upravených záznamov', -'Available databases and tables': 'Dostupné databázy a tabuľky', -'Cannot be empty': 'Nemôže byť prázdne', -'Check to delete': 'Označiť na zmazanie', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Current request': 'Aktuálna požiadavka', -'Current response': 'Aktuálna odpoveď', -'Current session': 'Aktuálne sedenie', -'DB Model': 'DB Model', -'Database': 'Databáza', -'Delete:': 'Zmazať:', -'Description': 'Popis', -'Edit': 'Upraviť', -'Edit Profile': 'Upraviť profil', -'Edit current record': 'Upraviť aktuálny záznam', -'First name': 'Krstné meno', -'Group ID': 'ID skupiny', -'Hello World': 'Ahoj svet', -'Import/Export': 'Import/Export', -'Index': 'Index', -'Internal State': 'Vnútorný stav', -'Invalid email': 'Neplatný email', -'Invalid Query': 'Neplatná otázka', -'Invalid password': 'Nesprávne heslo', -'Last name': 'Priezvisko', -'Layout': 'Layout', -'Logged in': 'Prihlásený', -'Logged out': 'Odhlásený', -'Lost Password': 'Stratené heslo?', -'Menu Model': 'Menu Model', -'Name': 'Meno', -'New Record': 'Nový záznam', -'New password': 'Nové heslo', -'No databases in this application': 'V tejto aplikácii nie sú databázy', -'Old password': 'Staré heslo', -'Origin': 'Pôvod', -'Password': 'Heslo', -'Powered by': 'Powered by', -'Query:': 'Otázka:', -'Record ID': 'ID záznamu', -'Register': 'Zaregistrovať sa', -'Registration key': 'Registračný kľúč', -'Remember me (for 30 days)': 'Zapamätaj si ma (na 30 dní)', -'Reset Password key': 'Nastaviť registračný kľúč', -'Role': 'Rola', -'Rows in table': 'riadkov v tabuľke', -'Rows selected': 'označených riadkov', -'Submit': 'Odoslať', -'Stylesheet': 'Stylesheet', -'Sure you want to delete this object?': 'Ste si istí, že chcete zmazať tento objekt?', -'Table name': 'Názov tabuľky', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"query" je podmienka ako "db.table1.field1==\'value\'". Niečo ako "db.table1.field1==db.table2.field2" má za výsledok SQL JOIN.', -'The output of the file is a dictionary that was rendered by the view': 'Výstup zo súboru je slovník, ktorý bol zobrazený vo view', -'This is a copy of the scaffolding application': 'Toto je kópia skeletu aplikácie', -'Timestamp': 'Časová pečiatka', -'Update:': 'Upraviť:', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Použite (...)&(...) pre AND, (...)|(...) pre OR a ~(...) pre NOT na poskladanie komplexnejších otázok.', -'User %(id)s Logged-in': 'Používateľ %(id)s prihlásený', -'User %(id)s Logged-out': 'Používateľ %(id)s odhlásený', -'User %(id)s Password changed': 'Používateľ %(id)s zmenil heslo', -'User %(id)s Profile updated': 'Používateľ %(id)s upravil profil', -'User %(id)s Registered': 'Používateľ %(id)s sa zaregistroval', -'User ID': 'ID používateľa', -'Verify Password': 'Zopakujte heslo', -'View': 'Zobraziť', -'Welcome to web2py': 'Vitajte vo web2py', -'Which called the function': 'Ktorý zavolal funkciu', -'You are successfully running web2py': 'Úspešne ste spustili web2py', -'You can modify this application and adapt it to your needs': 'Môžete upraviť túto aplikáciu a prispôsobiť ju svojim potrebám', -'You visited the url': 'Navštívili ste URL', -'appadmin is disabled because insecure channel': 'appadmin je zakázaný bez zabezpečeného spojenia', -'cache': 'cache', -'Online examples': 'pre online príklady kliknite sem', -'Administrative interface': 'pre administrátorské rozhranie kliknite sem', -'customize me!': 'prispôsob ma!', -'data uploaded': 'údaje naplnené', -'database': 'databáza', -'database %s select': 'databáza %s výber', -'db': 'db', -'design': 'návrh', -'Documentation': 'Dokumentácia', -'done!': 'hotovo!', -'export as csv file': 'exportovať do csv súboru', -'insert new': 'vložiť nový záznam ', -'insert new %s': 'vložiť nový záznam %s', -'invalid request': 'Neplatná požiadavka', -'located in the file': 'nachádzajúci sa v súbore ', -'login': 'prihlásiť', -'logout': 'odhlásiť', -'lost password?': 'stratené heslo?', -'new record inserted': 'nový záznam bol vložený', -'next 100 rows': 'ďalších 100 riadkov', -'or import from csv file': 'alebo naimportovať z csv súboru', -'password': 'heslo', -'previous 100 rows': 'predchádzajúcich 100 riadkov', -'record': 'záznam', -'record does not exist': 'záznam neexistuje', -'record id': 'id záznamu', -'register': 'registrovať', -'selected': 'označených', -'state': 'stav', -'table': 'tabuľka', -'unable to parse csv file': 'nedá sa načítať csv súbor', -} DELETED applications/welcome/languages/zh-tw.py Index: applications/welcome/languages/zh-tw.py ================================================================== --- applications/welcome/languages/zh-tw.py +++ applications/welcome/languages/zh-tw.py @@ -1,165 +0,0 @@ -# coding: utf8 -{ -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"更新" 是選擇性的條件式, 格式就像 "欄位1=\'值\'". 但是 JOIN 的資料不可以使用 update 或是 delete"', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'%s rows deleted': '已刪除 %s 筆', -'%s rows updated': '已更新 %s 筆', -'(something like "it-it")': '(格式類似 "zh-tw")', -'A new version of web2py is available': '新版的 web2py 已發行', -'A new version of web2py is available: %s': '新版的 web2py 已發行: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': '注意: 登入管理帳號需要安全連線(HTTPS)或是在本機連線(localhost).', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': '注意: 因為在測試模式不保證多執行緒安全性,也就是說不可以同時執行多個測試案例', -'ATTENTION: you cannot edit the running application!': '注意:不可編輯正在執行的應用程式!', -'About': '關於', -'About application': '關於本應用程式', -'Admin is disabled because insecure channel': '管理功能(Admin)在不安全連線環境下自動關閉', -'Admin is disabled because unsecure channel': '管理功能(Admin)在不安全連線環境下自動關閉', -'Administrator Password:': '管理員密碼:', -'Are you sure you want to delete file "%s"?': '確定要刪除檔案"%s"?', -'Are you sure you want to uninstall application "%s"': '確定要移除應用程式 "%s"', -'Are you sure you want to uninstall application "%s"?': '確定要移除應用程式 "%s"', -'Authentication': '驗證', -'Available databases and tables': '可提供的資料庫和資料表', -'Cannot be empty': '不可空白', -'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '無法編譯:應用程式中含有錯誤,請除錯後再試一次.', -'Change Password': '變更密碼', -'Check to delete': '打勾代表刪除', -'Check to delete:': '點選以示刪除:', -'Client IP': '客戶端網址(IP)', -'Controller': '控件', -'Controllers': '控件', -'Copyright': '版權所有', -'Create new application': '創建應用程式', -'Current request': '目前網路資料要求(request)', -'Current response': '目前網路資料回應(response)', -'Current session': '目前網路連線資訊(session)', -'DB Model': '資料庫模組', -'DESIGN': '設計', -'Database': '資料庫', -'Date and Time': '日期和時間', -'Delete': '刪除', -'Delete:': '刪除:', -'Deploy on Google App Engine': '配置到 Google App Engine', -'Description': '描述', -'Design for': '設計為了', -'E-mail': '電子郵件', -'EDIT': '編輯', -'Edit': '編輯', -'Edit Profile': '編輯設定檔', -'Edit This App': '編輯本應用程式', -'Edit application': '編輯應用程式', -'Edit current record': '編輯當前紀錄', -'Editing file': '編輯檔案', -'Editing file "%s"': '編輯檔案"%s"', -'Error logs for "%(app)s"': '"%(app)s"的錯誤紀錄', -'First name': '名', -'Functions with no doctests will result in [passed] tests.': '沒有 doctests 的函式會顯示 [passed].', -'Group ID': '群組編號', -'Hello World': '嗨! 世界', -'Import/Export': '匯入/匯出', -'Index': '索引', -'Installed applications': '已安裝應用程式', -'Internal State': '內部狀態', -'Invalid Query': '不合法的查詢', -'Invalid action': '不合法的動作(action)', -'Invalid email': '不合法的電子郵件', -'Language files (static strings) updated': '語言檔已更新', -'Languages': '各國語言', -'Last name': '姓', -'Last saved on:': '最後儲存時間:', -'Layout': '網頁配置', -'License for': '軟體版權為', -'Login': '登入', -'Login to the Administrative Interface': '登入到管理員介面', -'Logout': '登出', -'Lost Password': '密碼遺忘', -'Main Menu': '主選單', -'Menu Model': '選單模組(menu)', -'Models': '資料模組', -'Modules': '程式模組', -'NO': '否', -'Name': '名字', -'New Record': '新紀錄', -'No databases in this application': '這應用程式不含資料庫', -'Origin': '原文', -'Original/Translation': '原文/翻譯', -'Password': '密碼', -"Password fields don't match": '密碼欄不匹配', -'Peeking at file': '選擇檔案', -'Powered by': '基於以下技術構建:', -'Query:': '查詢:', -'Record ID': '紀錄編號', -'Register': '註冊', -'Registration key': '註冊金鑰', -'Remember me (for 30 days)': '記住我(30 天)', -'Reset Password key': '重設密碼', -'Resolve Conflict file': '解決衝突檔案', -'Role': '角色', -'Rows in table': '在資料表裏的資料', -'Rows selected': '筆資料被選擇', -'Saved file hash:': '檔案雜湊值已紀錄:', -'Static files': '靜態檔案', -'Stylesheet': '網頁風格檔', -'Submit': '傳送', -'Sure you want to delete this object?': '確定要刪除此物件?', -'Table name': '資料表名稱', -'Testing application': '測試中的應用程式', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"查詢"是一個像 "db.表1.欄位1==\'值\'" 的條件式. 以"db.表1.欄位1==db.表2.欄位2"方式則相當於執行 JOIN SQL.', -'There are no controllers': '沒有控件(controllers)', -'There are no models': '沒有資料庫模組(models)', -'There are no modules': '沒有程式模組(modules)', -'There are no static files': '沒有靜態檔案', -'There are no translators, only default language is supported': '沒有翻譯檔,只支援原始語言', -'There are no views': '沒有視圖', -'This is the %(filename)s template': '這是%(filename)s檔案的樣板(template)', -'Ticket': '問題單', -'Timestamp': '時間標記', -'Unable to check for upgrades': '無法做升級檢查', -'Unable to download': '無法下載', -'Unable to download app': '無法下載應用程式', -'Update:': '更新:', -'Upload existing application': '更新存在的應用程式', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': '使用下列方式來組合更複雜的條件式, (...)&(...) 代表同時存在的條件, (...)|(...) 代表擇一的條件, ~(...)則代表反向條件.', -'User %(id)s Logged-in': '使用者 %(id)s 已登入', -'User %(id)s Registered': '使用者 %(id)s 已註冊', -'User ID': '使用者編號', -'Verify Password': '驗證密碼', -'View': '視圖', -'Views': '視圖', -'Welcome %s': '歡迎 %s', -'Welcome to web2py': '歡迎使用 web2py', -'YES': '是', -'about': '關於', -'appadmin is disabled because insecure channel': '因為來自非安全通道,管理介面關閉', -'cache': '快取記憶體', -'change password': '變更密碼', -'Online examples': '點此處進入線上範例', -'Administrative interface': '點此處進入管理介面', -'customize me!': '請調整我!', -'data uploaded': '資料已上傳', -'database': '資料庫', -'database %s select': '已選擇 %s 資料庫', -'db': 'db', -'design': '設計', -'done!': '完成!', -'edit profile': '編輯設定檔', -'export as csv file': '以逗號分隔檔(csv)格式匯出', -'insert new': '插入新資料', -'insert new %s': '插入新資料 %s', -'invalid request': '不合法的網路要求(request)', -'login': '登入', -'logout': '登出', -'new record inserted': '已插入新紀錄', -'next 100 rows': '往後 100 筆', -'or import from csv file': '或是從逗號分隔檔(CSV)匯入', -'previous 100 rows': '往前 100 筆', -'record': '紀錄', -'record does not exist': '紀錄不存在', -'record id': '紀錄編號', -'register': '註冊', -'selected': '已選擇', -'state': '狀態', -'table': '資料表', -'unable to parse csv file': '無法解析逗號分隔檔(csv)', -} DELETED applications/welcome/models/db.py Index: applications/welcome/models/db.py ================================================================== --- applications/welcome/models/db.py +++ applications/welcome/models/db.py @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# this file is released under public domain and you can use without limitations - -######################################################################### -## This scaffolding model makes your app work on Google App Engine too -######################################################################### - -if request.env.web2py_runtime_gae: # if running on Google App Engine - db = DAL('google:datastore') # connect to Google BigTable - # optional DAL('gae://namespace') - session.connect(request, response, db = db) # and store sessions and tickets there - ### or use the following lines to store sessions in Memcache - # from gluon.contrib.memdb import MEMDB - # from google.appengine.api.memcache import Client - # session.connect(request, response, db = MEMDB(Client())) -else: # else use a normal relational database - db = DAL('sqlite://storage.sqlite') # if not, use SQLite or other DB - -# by default give a view/generic.extension to all actions from localhost -# none otherwise. a pattern can be 'controller/function.extension' -response.generic_patterns = ['*'] if request.is_local else [] - -######################################################################### -## Here is sample code if you need for -## - email capabilities -## - authentication (registration, login, logout, ... ) -## - authorization (role based authorization) -## - services (xml, csv, json, xmlrpc, jsonrpc, amf, rss) -## - crud actions -## (more options discussed in gluon/tools.py) -######################################################################### - -from gluon.tools import Mail, Auth, Crud, Service, PluginManager, prettydate -mail = Mail() # mailer -auth = Auth(db) # authentication/authorization -crud = Crud(db) # for CRUD helpers using auth -service = Service() # for json, xml, jsonrpc, xmlrpc, amfrpc -plugins = PluginManager() # for configuring plugins - -mail.settings.server = 'logging' or 'smtp.gmail.com:587' # your SMTP server -mail.settings.sender = 'you@gmail.com' # your email -mail.settings.login = 'username:password' # your credentials or None - -auth.settings.hmac_key = '' # before define_tables() -auth.define_tables() # creates all needed tables -auth.settings.mailer = mail # for user email verification -auth.settings.registration_requires_verification = False -auth.settings.registration_requires_approval = False -auth.messages.verify_email = 'Click on the link http://'+request.env.http_host+URL('default','user',args=['verify_email'])+'/%(key)s to verify your email' -auth.settings.reset_password_requires_verification = True -auth.messages.reset_password = 'Click on the link http://'+request.env.http_host+URL('default','user',args=['reset_password'])+'/%(key)s to reset your password' - -######################################################################### -## If you need to use OpenID, Facebook, MySpace, Twitter, Linkedin, etc. -## register with janrain.com, uncomment and customize following -# from gluon.contrib.login_methods.rpx_account import RPXAccount -# auth.settings.actions_disabled = \ -# ['register','change_password','request_reset_password'] -# auth.settings.login_form = RPXAccount(request, api_key='...',domain='...', -# url = "http://localhost:8000/%s/default/user/login" % request.application) -## other login methods are in gluon/contrib/login_methods -######################################################################### - -crud.settings.auth = None # =auth to enforce authorization on crud - -######################################################################### -## Define your tables below (or better in another model file) for example -## -## >>> db.define_table('mytable',Field('myfield','string')) -## -## Fields can be 'string','text','password','integer','double','boolean' -## 'date','time','datetime','blob','upload', 'reference TABLENAME' -## There is an implicit 'id integer autoincrement' field -## Consult manual for more options, validators, etc. -## -## More API examples for controllers: -## -## >>> db.mytable.insert(myfield='value') -## >>> rows=db(db.mytable.myfield=='value').select(db.mytable.ALL) -## >>> for row in rows: print row.id, row.myfield -######################################################################### DELETED applications/welcome/models/menu.py Index: applications/welcome/models/menu.py ================================================================== --- applications/welcome/models/menu.py +++ applications/welcome/models/menu.py @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -# this file is released under public domain and you can use without limitations -######################################################################### -## Customize your APP title, subtitle and menus here -######################################################################### - -response.title = request.application -response.subtitle = T('customize me!') - -#http://dev.w3.org/html5/markup/meta.name.html -response.meta.author = 'you' -response.meta.description = 'Free and open source full-stack enterprise framework for agile development of fast, scalable, secure and portable database-driven web-based applications. Written and programmable in Python' -response.meta.keywords = 'web2py, python, framework' -response.meta.generator = 'Web2py Enterprise Framework' -response.meta.copyright = 'Copyright 2007-2010' - - -########################################## -## this is the main application menu -## add/remove items as required -########################################## - -response.menu = [ - (T('Home'), False, URL('default','index'), []) - ] - -########################################## -## this is here to provide shortcuts -## during development. remove in production -## -## mind that plugins may also affect menu -########################################## - -######################################### -## Make your own menus -########################################## - -response.menu+=[ - (T('This App'), False, URL('admin', 'default', 'design/%s' % request.application), - [ - (T('Controller'), False, - URL('admin', 'default', 'edit/%s/controllers/%s.py' \ - % (request.application,request.controller=='appadmin' and - 'default' or request.controller))), - (T('View'), False, - URL('admin', 'default', 'edit/%s/views/%s' \ - % (request.application,response.view))), - (T('Layout'), False, - URL('admin', 'default', 'edit/%s/views/layout.html' \ - % request.application)), - (T('Stylesheet'), False, - URL('admin', 'default', 'edit/%s/static/base.css' \ - % request.application)), - (T('DB Model'), False, - URL('admin', 'default', 'edit/%s/models/db.py' \ - % request.application)), - (T('Menu Model'), False, - URL('admin', 'default', 'edit/%s/models/menu.py' \ - % request.application)), - (T('Database'), False, - URL(request.application, 'appadmin', 'index')), - - (T('Errors'), False, - URL('admin', 'default', 'errors/%s' \ - % request.application)), - - (T('About'), False, - URL('admin', 'default', 'about/%s' \ - % request.application)), - - ] - )] - - -########################################## -## this is here to provide shortcuts to some resources -## during development. remove in production -## -## mind that plugins may also affect menu -########################################## - - -response.menu+=[(T('Resources'), False, None, - [ - (T('Documentation'), False, 'http://www.web2py.com/book', - [ - (T('Preface'), False, 'http://www.web2py.com/book/default/chapter/00'), - (T('Introduction'), False, 'http://www.web2py.com/book/default/chapter/01'), - (T('Python'), False, 'http://www.web2py.com/book/default/chapter/02'), - (T('Overview'), False, 'http://www.web2py.com/book/default/chapter/03'), - (T('The Core'), False, 'http://www.web2py.com/book/default/chapter/04'), - (T('The Views'), False, 'http://www.web2py.com/book/default/chapter/05'), - (T('Database'), False, 'http://www.web2py.com/book/default/chapter/06'), - (T('Forms and Validators'), False, 'http://www.web2py.com/book/default/chapter/07'), - (T('Access Control'), False, 'http://www.web2py.com/book/default/chapter/08'), - (T('Services'), False, 'http://www.web2py.com/book/default/chapter/09'), - (T('Ajax Recipes'), False, 'http://www.web2py.com/book/default/chapter/10'), - (T('Deployment Recipes'), False, 'http://www.web2py.com/book/default/chapter/11'), - (T('Other Recipes'), False, 'http://www.web2py.com/book/default/chapter/12'), - (T('Buy this book'), False, 'http://stores.lulu.com/web2py'), - ]), - - (T('Community'), False, None, - [ - (T('Groups'), False, 'http://www.web2py.com/examples/default/usergroups'), - (T('Twitter'), False, 'http://twitter.com/web2py'), - (T('Live chat'), False, 'http://mibbit.com/?channel=%23web2py&server=irc.mibbit.net'), - (T('User Voice'), False, 'http://web2py.uservoice.com/'), - ]), - - (T('Web2py'), False, 'http://www.web2py.com', - [ - (T('Download'), False, 'http://www.web2py.com/examples/default/download'), - (T('Support'), False, 'http://www.web2py.com/examples/default/support'), - (T('Quick Examples'), False, 'http://web2py.com/examples/default/examples'), - (T('FAQ'), False, 'http://web2py.com/AlterEgo'), - (T('Free Applications'), False, 'http://web2py.com/appliances'), - (T('Plugins'), False, 'http://web2py.com/plugins'), - (T('Recipes'), False, 'http://web2pyslices.com/'), - (T('Demo'), False, 'http://web2py.com/demo_admin'), - (T('Semantic'), False, 'http://web2py.com/semantic'), - (T('Layouts'), False, 'http://web2py.com/layouts'), - (T('Videos'), False, 'http://www.web2py.com/examples/default/videos/'), - ]), - ] - )] DELETED applications/welcome/modules/__init__.py Index: applications/welcome/modules/__init__.py ================================================================== --- applications/welcome/modules/__init__.py +++ applications/welcome/modules/__init__.py DELETED applications/welcome/static/css/base.css Index: applications/welcome/static/css/base.css ================================================================== --- applications/welcome/static/css/base.css +++ applications/welcome/static/css/base.css @@ -1,556 +0,0 @@ -@charset "UTF-8"; - -/* This file is contains the following sections: - -Update: Revision: 20101102 by Martin Mulone - -- The new revision contains: -- Html5, good practice and normalization support. -- Diferent hacks. -- The normalization and some tags come from - diferent sites so i keep the credits and comments. - but the base of support html5 come from: - http://html5boilerplate.com/ - -- ez.css (http://www.ez-css.org/layouts) -- reset common tags -- choose default fonts -- choose link style -- add bottom line to table rows -- labels bold and occasionally centered -- make all input fields the same size -- add proper separation between h1-h6 and text -- always indent the first line and add space below paragraphs -- bullets and numbers style and indent -- form and table padding -- code blocks -- left and right padding to quoted text -- page layout alignment, width and padding (change this for spaces) -- column widths (change this to use left_sidebar and right_sidebar) -- backrgound images and colors (change this for colors) -- web2py specific (.flash, .error) - -Notice: -- even if you use a different layout/css you may need classes .flash and .error -- this is all color neutral except for #349C01 (header, links, lines) -- there are two backrgound images: images/background.png and images/header.png - -License: This file is released under BSD and MIT - -*/ - -/* - credit is left where credit is due. - additionally, much inspiration was taken from these projects: - yui.yahooapis.com/2.8.1/build/base/base.css - camendesign.com/design/ - praegnanz.de/weblog/htmlcssjs-kickstart -*/ - -/* - html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline) - v1.4 2009-07-27 | Authors: Eric Meyer & Richard Clark - html5doctor.com/html-5-reset-stylesheet/ -*/ - -html, body, div, span, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -abbr, address, cite, code, -del, dfn, em, img, ins, kbd, q, samp, -small, strong, sub, sup, var, -b, i, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, figcaption, figure, -footer, header, hgroup, menu, nav, section, summary, -time, mark, audio, video { - margin:0; - padding:0; - border:0; - outline:0; - font-size:100%; - vertical-align:baseline; - background:transparent; -} - -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display:block; -} - -nav ul { list-style:none; } - -blockquote, q { quotes:none; } - -blockquote:before, blockquote:after, -q:before, q:after { content:''; content:none; } - -a { margin:0; padding:0; font-size:100%; vertical-align:baseline; background:transparent; } -a:hover { text-decoration: underline } - -ins { background-color:#ff9; color:#000; text-decoration:none; } - -mark { background-color:#ff9; color:#000; font-style:italic; font-weight:bold; } - -del { text-decoration: line-through; } - -abbr[title], dfn[title] { border-bottom:1px dotted #000; cursor:help; } - -/* tables still need cellspacing="0" in the markup */ -table { border-collapse:collapse; border-spacing:0; } - -hr { display:block; height:1px; border:0; border-top:1px solid #ccc; margin:1em 0; padding:0; } - -input, select { vertical-align:middle; } -/* END RESET CSS */ - - -/* -fonts.css from the YUI Library: developer.yahoo.com/yui/ - Please refer to developer.yahoo.com/yui/fonts/ for font sizing percentages - -There are three custom edits: - * remove arial, helvetica from explicit font stack - * make the line-height relative and unit-less - * remove the pre, code styles -*/ -body { font:13px/1.231 sans-serif; *font-size:small; } /* hack retained to preserve specificity */ - -/*table { font-size:inherit; font:100%; }*/ - -select, input, textarea, button { font:99% sans-serif; } - - -/* normalize monospace sizing - * en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome - */ -pre, code, kbd, samp { font-family: monospace, sans-serif; } - -/* - * minimal base styles - */ - -/* #444 looks better than black: twitter.com/H_FJ/statuses/11800719859 */ -body, select, input, textarea { color:#444; } - -/* Headers (h1,h2,etc) have no default font-size or margin, - you'll want to define those yourself. */ - -/* www.aestheticallyloyal.com/public/optimize-legibility/ */ -h1,h2,h3,h4,h5,h6 { font-weight: bold; } - -/* always force a scrollbar in non-IE */ -html { overflow-y: scroll; } - -/* Accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */ -a:hover, a:active { outline: none; } - -a, a:active, a:visited { color:#607890; } -a:hover { color:#036; } - -ul, ol { margin-left: 1.8em; } -ol { list-style-type: decimal; } - -/* Remove margins for navigation lists */ -nav ul, nav li { margin: 0; } - -small { font-size:85%; } -strong, th { font-weight: bold; } - -td, td img { vertical-align:top; } - -sub { vertical-align: sub; font-size: smaller; } -sup { vertical-align: super; font-size: smaller; } - -pre { - padding: 15px; - - /* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */ - white-space: pre; /* CSS2 */ - white-space: pre-wrap; /* CSS 2.1 */ - white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ - word-wrap: break-word; /* IE */ -} - -textarea { overflow: auto; } /* thnx ivannikolic! www.sitepoint.com/blogs/2010/08/20/ie-remove-textarea-scrollbars/ */ - -.ie6 legend, .ie7 legend { margin-left: -7px; } /* thnx ivannikolic! */ - -/* align checkboxes, radios, text inputs with their label - by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css */ -input[type="radio"] { vertical-align: text-bottom; } -input[type="checkbox"] { vertical-align: bottom; } -.ie7 input[type="checkbox"] { vertical-align: baseline; } -.ie6 input { vertical-align: text-bottom; } - -/* hand cursor on clickable input elements */ -label, input[type=button], input[type=submit], button { cursor: pointer; } - -/* webkit browsers add a 2px margin outside the chrome of form elements */ -button, input, select, textarea { margin: 0; } - -/* colors for form validity */ -input:valid, textarea:valid { } -input:invalid, textarea:invalid { - border-radius: 1px; - -moz-box-shadow: 0px 0px 5px red; - -webkit-box-shadow: 0px 0px 5px red; - box-shadow: 0px 0px 5px red; -} -.no-boxshadow input:invalid, -.no-boxshadow textarea:invalid { background-color: #f0dddd; } - - -/* These selection declarations have to be separate. - No text-shadow: twitter.com/miketaylr/status/12228805301 - Also: hot pink. */ -::-moz-selection{ background: #555; color:#fff; text-shadow: none; } -::selection { background:#555; color:#fff; text-shadow: none; } - -/* j.mp/webkit-tap-highlight-color */ -a:link { -webkit-tap-highlight-color: #555; } - - -/* make buttons play nice in IE: - www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */ -button { width: auto; overflow: visible; } - -/* bicubic resizing for non-native sized IMG: - code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */ -.ie7 img { -ms-interpolation-mode: bicubic; } - -/* - * Non-semantic helper classes - */ - -/* for image replacement */ -.ir { display:block; text-indent:-999em; overflow:hidden; background-repeat: no-repeat; } - -/* Hide for both screenreaders and browsers - css-discuss.incutio.com/wiki/Screenreader_Visibility */ -.hidden { display:none; } - -/* Hide only visually, but have it available for screenreaders - www.webaim.org/techniques/css/invisiblecontent/ - Solution from: j.mp/visuallyhidden - Thanks Jonathan Neal! */ -.visuallyhidden { position:absolute !important; - clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ - clip: rect(1px, 1px, 1px, 1px); } - -/* Hide visually and from screenreaders, but maintain layout */ -.invisible { visibility: hidden; } - -/* >> The Magnificent CLEARFIX: Updated to prevent margin-collapsing on child elements << j.mp/bestclearfix */ -.clearfix:before, .clearfix:after { - content: "\0020"; display: block; height: 0; visibility: hidden; -} - -.clearfix:after { clear: both; } -/* Fix clearfix: blueprintcss.lighthouseapp.com/projects/15318/tickets/5-extra-margin-padding-bottom-of-page */ -.clearfix { zoom: 1; } - - -/*********** layout info (ez.css) ***********/ -/* 2009 -2010 (c) | ez-css.org - * ez-plug-min.css :: version 1.1 :: 01182010 - */ -.ez-wr:after,.ez-box:after{content:".";display:block;height:0;clear:both;visibility:hidden}.ez-wr,.ez-box,.ez-last{display:inline-block;min-height:0}/* \*/ * html .ez-wr,* html .ez-box,* html .ez-last{height:1%}.ez-wr,.ez-box,.ez-last{display:block}/* */.ez-oh{overflow:hidden}* html .ez-oh{overflow:visible}.ez-oa{overflow:auto}.ez-dt{display:table}.ez-it{display:inline-table}.ez-tc{display:table-cell}.ez-ib{display:inline-block}.ez-fl{float:left}* html .ez-fl{margin-right:-3px}.ez-fr{float:right}* html .ez-fr{margin-left:-3px}.ez-25{width:25%}.ez-33{width:33.33%}.ez-50{width:50%}.ez-66{width:66.66%}.ez-75{width:75%}.ez-negmr{margin-right:-1px}* html .ez-negmr{margin-right:-4px}.ez-negmx{margin-right:-1px}.ez-negml{margin-left:-1px}* html .ez-negml{margin-left:-4px} - - -/*********** add bottom line to table rows ***********/ -th, td { padding: 0.1em 0.5em 0.1em 0.5em;} - -/*********** labels bold and occasionally centered ***********/ -label { - white-space: nowrap; -} -label, b, th { - font-weight: bold; -} -thead th { - text-align: center; - border-bottom: 1px solid #444; -} -/*********** forms and table padding ***********/ -form, table { - padding: 5px 10px 5px 10px; -} - -/*********** code blocks ***********/ -code { - padding: 3px 5px; - font-family: Andale Mono, monospace; - font-size: 0.9em; -} - -/*********** left and right padding to quoted text ***********/ -blockquote { - background: #cccccc; - border-left: 30px transparent; - border-right: 30px transparent; - /*padding: 5px;*/ -} - -input[type=text], input[type=password], textarea, select { - margin: 2px 15px 2px 5px; - width: 280px; - background: #fff; - color: #555; - border: 1px solid #dedede; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; - font-size: 12px; -} - -input[type=text], input[type=password] { - height: 16px; -} - -select[multiple=multiple] { - height: 90px; -} - -input[type=submit], input[type=button], button { - margin: 0px; - /*width: 85px;*/ - height: 22px; - background: #eaeaea; - color: #555; - border: 1px solid #dedede; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; -} - -fieldset { border: 1px solid #dedede; padding: 6px; } -legend { font-weight: bold; } - -input:focus, textarea:focus { background: #fafafa; } - -p {text-indent:30px;} - -p, blockquote { - margin-bottom: 10px; -} - -h1,h2,h3,h4,h5,h6 { line-height: 170%; } -h1 {font-size: 2.0em;} -h2 {font-size: 1.8em;} -h3 {font-size: 1.4em;} -h4 {font-size: 1.2em;} -h5 {font-size: 1.0em;} -h6 {font-size: 0.8em;} - -/*********** page layout alignment, width and padding ***********/ -/*body {background-color: #000;}*/ -#container, #header, #page, #content, #statusbar, -#footer, #wrapper { display:block; line-height: 170%; } -#wrapper {width: 900px;} -#container { - margin: 0 auto; - padding: 0; -} -#wrapper {margin: 0 auto;} -#wrapper {background-color: #fff; padding: 5px;} -#statusbar { margin: 5px 0px 20px 0px;} -#footer { - margin-top: 30px; - padding: 5px; -} -#statusbar, #footer { - background: #eaeaea; - border-top: 1px #aaa solid; -} -#logo { - width: 68px; - height: 62px; - background: url(../images/logo.png); -} -#appname { - color: #cccccc; -} - -#right_sidebar { width: 160px; float:right; display: none; } -#left_sidebar { width: 160px; float:left; display: none; } -#content { float: left; /*width: 740px;*//*width: 63%;*/ /*width: 640px; float:left;*/ } /* uncomment this if you are going to use sidebars */ - -.auth_navbar { - top: 0px; - float: right; - padding: 3px 10px 3px 10px; -} - -/*********** web2py specific ***********/ -div.flash { - font-weight: bold; - display: none; - position: fixed; - padding: 10px; - top: 40px; - right: 10px; - min-width: 280px; - opacity: 0.85; - margin: 0px 0px 10px 10px; - color: #fff; - vertical-align: middle; - cursor: pointer; - background: #000; - border: 2px solid #fff; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - z-index: 2; -} -div.error { - background-color: red; - color: white; - padding: 3px; -} - -/*************************** - * CSS 3 Buttons - * http://github.com/michenriksen/css3buttons - * created by Michael Henriksen - * License: Unlicense - * - * *******************/ - -a.button { display: inline-block; padding: 3px 5px 3px 5px; font-family: 'lucida grande', tahoma, verdana, arial, sans-serif; font-size: 12px; color: #3C3C3D; text-shadow: 1px 1px 0 #FFFFFF; background: #ECECEC url('../images/css3buttons_backgrounds.png') 0 0 no-repeat; white-space: nowrap; overflow: visible; cursor: pointer; text-decoration: none; border: 1px solid #CACACA; -webkit-border-radius: 2px; -moz-border-radius: 2px; -webkit-background-clip: padding-box; border-radius: 2px; outline: none; position: relative; zoom: 1; *display: inline; } -a.button.primary { font-weight: bold } -a.button:hover { color: #FFFFFF; border-color: #388AD4; text-decoration: none; text-shadow: -1px -1px 0 rgba(0,0,0,0.3); background-position: 0 -40px; background-color: #2D7DC5; } -a.button:active, -a.button.active { background-position: 0 -81px; border-color: #347BBA; background-color: #0F5EA2; color: #FFFFFF; text-shadow: none; } -a.button:active { top: 1px } -a.button.negative:hover { color: #FFFFFF; background-position: 0 -121px; background-color: #D84743; border-color: #911D1B; } -a.button.negative:active, -a.button.negative.active { background-position: 0 -161px; background-color: #A5211E; border-color: #911D1B; } -a.button.pill { -webkit-border-radius: 19px; -moz-border-radius: 19px; border-radius: 19px; padding: 2px 10px 2px 10px; } -a.button.left { -webkit-border-bottom-right-radius: 0px; -webkit-border-top-right-radius: 0px; -moz-border-radius-bottomright: 0px; -moz-border-radius-topright: 0px; border-bottom-right-radius: 0px; border-top-right-radius: 0px; margin-right: 0px; } -a.button.middle { margin-right: 0px; margin-left: 0px; -webkit-border-radius: 0px; -moz-border-radius: 0px; border-radius: 0px; border-right: none; border-left: none; } -a.button.right { -webkit-border-bottom-left-radius: 0px; -webkit-border-top-left-radius: 0px; -moz-border-radius-bottomleft: 0px; -moz-border-radius-topleft: 0px; border-top-left-radius: 0px; border-bottom-left-radius: 0px; margin-left: 0px; } -a.button.left:active, -a.button.middle:active, -a.button.right:active { top: 0px } -a.button.big { font-size: 16px; padding-left: 17px; padding-right: 17px; } -a.button span.icon { display: inline-block; width: 14px; height: 12px; margin: auto 7px auto auto; position: relative; top: 2px; background-image: url('../images/css3buttons_icons.png'); background-repeat: no-repeat; } -a.big.button span.icon { top: 0px } -a.button span.icon.book { background-position: 0 0 } -a.button:hover span.icon.book { background-position: 0 -15px } -a.button span.icon.calendar { background-position: 0 -30px } -a.button:hover span.icon.calendar { background-position: 0 -45px } -a.button span.icon.chat { background-position: 0 -60px } -a.button:hover span.icon.chat { background-position: 0 -75px } -a.button span.icon.check { background-position: 0 -90px } -a.button:hover span.icon.check { background-position: 0 -103px } -a.button span.icon.clock { background-position: 0 -116px } -a.button:hover span.icon.clock { background-position: 0 -131px } -a.button span.icon.cog { background-position: 0 -146px } -a.button:hover span.icon.cog { background-position: 0 -161px } -a.button span.icon.comment { background-position: 0 -176px } -a.button:hover span.icon.comment { background-position: 0 -190px } -a.button span.icon.cross { background-position: 0 -204px } -a.button:hover span.icon.cross { background-position: 0 -219px } -a.button span.icon.downarrow { background-position: 0 -234px } -a.button:hover span.icon.downarrow { background-position: 0 -249px } -a.button span.icon.fork { background-position: 0 -264px } -a.button:hover span.icon.fork { background-position: 0 -279px } -a.button span.icon.heart { background-position: 0 -294px } -a.button:hover span.icon.heart { background-position: 0 -308px } -a.button span.icon.home { background-position: 0 -322px } -a.button:hover span.icon.home { background-position: 0 -337px } -a.button span.icon.key { background-position: 0 -352px } -a.button:hover span.icon.key { background-position: 0 -367px } -a.button span.icon.leftarrow { background-position: 0 -382px } -a.button:hover span.icon.leftarrow { background-position: 0 -397px } -a.button span.icon.lock { background-position: 0 -412px } -a.button:hover span.icon.lock { background-position: 0 -427px } -a.button span.icon.loop { background-position: 0 -442px } -a.button:hover span.icon.loop { background-position: 0 -457px } -a.button span.icon.magnifier { background-position: 0 -472px } -a.button:hover span.icon.magnifier { background-position: 0 -487px } -a.button span.icon.mail { background-position: 0 -502px } -a.button:hover span.icon.mail { background-position: 0 -514px } -a.button span.icon.move { background-position: 0 -526px } -a.button:hover span.icon.move { background-position: 0 -541px } -a.button span.icon.pen { background-position: 0 -556px } -a.button:hover span.icon.pen { background-position: 0 -571px } -a.button span.icon.pin { background-position: 0 -586px } -a.button:hover span.icon.pin { background-position: 0 -601px } -a.button span.icon.plus { background-position: 0 -616px } -a.button:hover span.icon.plus { background-position: 0 -631px } -a.button span.icon.reload { background-position: 0 -646px } -a.button:hover span.icon.reload { background-position: 0 -660px } -a.button span.icon.rightarrow { background-position: 0 -674px } -a.button:hover span.icon.rightarrow { background-position: 0 -689px } -a.button span.icon.rss { background-position: 0 -704px } -a.button:hover span.icon.rss { background-position: 0 -719px } -a.button span.icon.tag { background-position: 0 -734px } -a.button:hover span.icon.tag { background-position: 0 -749px } -a.button span.icon.trash { background-position: 0 -764px } -a.button:hover span.icon.trash { background-position: 0 -779px } -a.button span.icon.unlock { background-position: 0 -794px } -a.button:hover span.icon.unlock { background-position: 0 -809px } -a.button span.icon.uparrow { background-position: 0 -824px } -a.button:hover span.icon.uparrow { background-position: 0 -839px } -a.button span.icon.user { background-position: 0 -854px } -a.button:hover span.icon.user { background-position: 0 -869px } - - - - - - -/***************************************************** - * HERE YOU CAN START TO WRITE YOUR OWN DIVS - */ - - - - - - - -/* - * Media queries for responsive design - */ - -@media all and (orientation:portrait) { - /* Style adjustments for portrait mode goes here */ - -} - -@media all and (orientation:landscape) { - /* Style adjustments for landscape mode goes here */ - -} - -/* Grade-A Mobile Browsers (Opera Mobile, iPhone Safari, Android Chrome) - Consider this: www.cloudfour.com/css-media-query-for-mobile-is-fools-gold/ */ -@media screen and (max-device-width: 480px) { - - - /* Uncomment if you don't want iOS and WinMobile to mobile-optimize the text for you - j.mp/textsizeadjust - html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; } */ -} - - -/* - * print styles - * inlined to avoid required HTTP connection www.phpied.com/delay-loading-your-print-css/ - */ -@media print { - * { background: transparent !important; color: #444 !important; text-shadow: none !important; } - a, a:visited { color: #444 !important; text-decoration: underline; } - a:after { content: " (" attr(href) ")"; } - abbr:after { content: " (" attr(title) ")"; } - .ir a:after { content: ""; } /* Don't show links for images */ - pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } - thead { display: table-header-group; } /* css-discuss.incutio.com/wiki/Printing_Tables */ - tr, img { page-break-inside: avoid; } - @page { margin: 0.5cm; } - p, h2, h3 { orphans: 3; widows: 3; } - h2, h3{ page-break-after: avoid; } -} - DELETED applications/welcome/static/css/calendar.css Index: applications/welcome/static/css/calendar.css ================================================================== --- applications/welcome/static/css/calendar.css +++ applications/welcome/static/css/calendar.css @@ -1,1 +0,0 @@ -.calendar{z-index:99;position:relative;display:none;border-top:2px solid #fff;border-right:2px solid #000;border-bottom:2px solid #000;border-left:2px solid #fff;font-size:11px;color:#000;cursor:default;background:#d4d0c8;font-family:tahoma,verdana,sans-serif;}.calendar table{border-top:1px solid #000;border-right:1px solid #fff;border-bottom:1px solid #fff;border-left:1px solid #000;font-size:11px;color:#000;cursor:default;background:#d4d0c8;font-family:tahoma,verdana,sans-serif;}.calendar .button{text-align:center;padding:1px;border-top:1px solid #fff;border-right:1px solid #000;border-bottom:1px solid #000;border-left:1px solid #fff;}.calendar .nav{background:transparent}.calendar thead .title{font-weight:bold;padding:1px;border:1px solid #000;background:#848078;color:#fff;text-align:center;}.calendar thead .name{border-bottom:1px solid #000;padding:2px;text-align:center;background:#f4f0e8;}.calendar thead .weekend{color:#f00;}.calendar thead .hilite{border-top:2px solid #fff;border-right:2px solid #000;border-bottom:2px solid #000;border-left:2px solid #fff;padding:0;background-color:#e4e0d8;}.calendar thead .active{padding:2px 0 0 2px;border-top:1px solid #000;border-right:1px solid #fff;border-bottom:1px solid #fff;border-left:1px solid #000;background-color:#c4c0b8;}.calendar tbody .day{width:2em;text-align:right;padding:2px 4px 2px 2px;}.calendar tbody .day.othermonth{font-size:80%;color:#aaa;}.calendar tbody .day.othermonth.oweekend{color:#faa;}.calendar table .wn{padding:2px 3px 2px 2px;border-right:1px solid #000;background:#f4f0e8;}.calendar tbody .rowhilite td{background:#e4e0d8;}.calendar tbody .rowhilite td.wn{background:#d4d0c8;}.calendar tbody td.hilite{padding:1px 3px 1px 1px;border-top:1px solid #fff;border-right:1px solid #000;border-bottom:1px solid #000;border-left:1px solid #fff;}.calendar tbody td.active{padding:2px 2px 0 2px;border-top:1px solid #000;border-right:1px solid #fff;border-bottom:1px solid #fff;border-left:1px solid #000;}.calendar tbody td.selected{font-weight:bold;border-top:1px solid #000;border-right:1px solid #fff;border-bottom:1px solid #fff;border-left:1px solid #000;padding:2px 2px 0 2px;background:#e4e0d8;}.calendar tbody td.weekend{color:#f00;}.calendar tbody td.today{font-weight:bold;color:#00f;}.calendar tbody .disabled{color:#999;}.calendar tbody .emptycell{visibility:hidden;}.calendar tbody .emptyrow{display:none;}.calendar tfoot .ttip{background:#f4f0e8;padding:1px;border:1px solid #000;background:#848078;color:#fff;text-align:center;}.calendar tfoot .hilite{border-top:1px solid #fff;border-right:1px solid #000;border-bottom:1px solid #000;border-left:1px solid #fff;padding:1px;background:#e4e0d8;}.calendar tfoot .active{padding:2px 0 0 2px;border-top:1px solid #000;border-right:1px solid #fff;border-bottom:1px solid #fff;border-left:1px solid #000;}.calendar .combo{position:absolute;display:none;width:4em;top:0;left:0;cursor:default;border-top:1px solid #fff;border-right:1px solid #000;border-bottom:1px solid #000;border-left:1px solid #fff;background:#e4e0d8;font-size:90%;padding:1px;z-index:100;}.calendar .combo .label,.calendar .combo .label-IEfix{text-align:center;padding:1px;}.calendar .combo .label-IEfix{width:4em;}.calendar .combo .active{background:#c4c0b8;padding:0;border-top:1px solid #000;border-right:1px solid #fff;border-bottom:1px solid #fff;border-left:1px solid #000;}.calendar .combo .hilite{background:#048;color:#fea;}.calendar td.time{border-top:1px solid #000;padding:1px 0;text-align:center;background-color:#f4f0e8;}.calendar td.time .hour,.calendar td.time .minute,.calendar td.time .ampm{padding:0 3px 0 4px;border:1px solid #889;font-weight:bold;background-color:#fff;}.calendar td.time .ampm{text-align:center;}.calendar td.time .colon{padding:0 2px 0 3px;font-weight:bold;}.calendar td.time span.hilite{border-color:#000;background-color:#766;color:#fff;}.calendar td.time span.active{border-color:#f00;background-color:#000;color:#0f0;}#CP_hourcont{z-index:99;padding:0;position:absolute;border:1px dashed #666;background-color:#eee;display:none;}#CP_minutecont{z-index:99;background-color:#ddd;padding:1px;position:absolute;width:45px;display:none;}.floatleft{float:left;}.CP_hour{z-index:99;padding:1px;font-family:Arial,Helvetica,sans-serif;font-size:9px;white-space:nowrap;cursor:pointer;width:35px;}.CP_minute{z-index:99;padding:1px;font-family:Arial,Helvetica,sans-serif;font-size:9px;white-space:nowrap;cursor:pointer;width:auto;}.CP_over{background-color:#fff;z-index:99} DELETED applications/welcome/static/css/handheld.css Index: applications/welcome/static/css/handheld.css ================================================================== --- applications/welcome/static/css/handheld.css +++ applications/welcome/static/css/handheld.css @@ -1,7 +0,0 @@ - -* { - float: none; /* Screens are not big enough to account for floats */ - font-size: 80%; /* Slightly reducing font size to reduce need to scroll */ - background: #fff; /* As much contrast as possible */ - color: #000; -} DELETED applications/welcome/static/css/superfish-navbar.css Index: applications/welcome/static/css/superfish-navbar.css ================================================================== --- applications/welcome/static/css/superfish-navbar.css +++ applications/welcome/static/css/superfish-navbar.css @@ -1,93 +0,0 @@ - -/*** adding the class sf-navbar in addition to sf-menu creates an all-horizontal nav-bar menu ***/ -.sf-navbar { - background: #BDD2FF; - height: 2.5em; - padding-bottom: 2.5em; - position: relative; -} -.sf-navbar li { - background: #AABDE6; - position: static; -} -.sf-navbar a { - border-top: none; -} -.sf-navbar li ul { - width: 44em; /*IE6 soils itself without this*/ -} -.sf-navbar li li { - background: #BDD2FF; - position: relative; -} -.sf-navbar li li ul { - width: 13em; -} -.sf-navbar li li li { - width: 100%; -} -.sf-navbar ul li { - width: auto; - float: left; -} -.sf-navbar a, .sf-navbar a:visited { - border: none; -} -.sf-navbar li.current { - background: #BDD2FF; -} -.sf-navbar li:hover, -.sf-navbar li.sfHover, -.sf-navbar li li.current, -.sf-navbar a:focus, .sf-navbar a:hover, .sf-navbar a:active { - background: #BDD2FF; -} -.sf-navbar ul li:hover, -.sf-navbar ul li.sfHover, -ul.sf-navbar ul li:hover li, -ul.sf-navbar ul li.sfHover li, -.sf-navbar ul a:focus, .sf-navbar ul a:hover, .sf-navbar ul a:active { - background: #D1DFFF; -} -ul.sf-navbar li li li:hover, -ul.sf-navbar li li li.sfHover, -.sf-navbar li li.current li.current, -.sf-navbar ul li li a:focus, .sf-navbar ul li li a:hover, .sf-navbar ul li li a:active { - background: #E6EEFF; -} -ul.sf-navbar .current ul, -ul.sf-navbar ul li:hover ul, -ul.sf-navbar ul li.sfHover ul { - left: 0; - top: 2.5em; /* match top ul list item height */ -} -ul.sf-navbar .current ul ul { - top: -999em; -} - -.sf-navbar li li.current > a { - font-weight: bold; -} - -/*** point all arrows down ***/ -/* point right for anchors in subs */ -.sf-navbar ul .sf-sub-indicator { background-position: -10px -100px; } -.sf-navbar ul a > .sf-sub-indicator { background-position: 0 -100px; } -/* apply hovers to modern browsers */ -.sf-navbar ul a:focus > .sf-sub-indicator, -.sf-navbar ul a:hover > .sf-sub-indicator, -.sf-navbar ul a:active > .sf-sub-indicator, -.sf-navbar ul li:hover > a > .sf-sub-indicator, -.sf-navbar ul li.sfHover > a > .sf-sub-indicator { - background-position: -10px -100px; /* arrow hovers for modern browsers*/ -} - -/*** remove shadow on first submenu ***/ -.sf-navbar > li > ul { - background: transparent; - padding: 0; - -moz-border-radius-bottomleft: 0; - -moz-border-radius-topright: 0; - -webkit-border-top-right-radius: 0; - -webkit-border-bottom-left-radius: 0; -} DELETED applications/welcome/static/css/superfish-vertical.css Index: applications/welcome/static/css/superfish-vertical.css ================================================================== --- applications/welcome/static/css/superfish-vertical.css +++ applications/welcome/static/css/superfish-vertical.css @@ -1,23 +0,0 @@ -/*** adding sf-vertical in addition to sf-menu creates a vertical menu ***/ -.sf-vertical, .sf-vertical li { - width: 10em; -} -/* this lacks ul at the start of the selector, so the styles from the main CSS file override it where needed */ -.sf-vertical li:hover ul, -.sf-vertical li.sfHover ul { - left: 10em; /* match ul width */ - top: 0; -} - -/*** alter arrow directions ***/ -.sf-vertical .sf-sub-indicator { background-position: -10px 0; } /* IE6 gets solid image only */ -.sf-vertical a > .sf-sub-indicator { background-position: 0 0; } /* use translucent arrow for modern browsers*/ - -/* hover arrow direction for modern browsers*/ -.sf-vertical a:focus > .sf-sub-indicator, -.sf-vertical a:hover > .sf-sub-indicator, -.sf-vertical a:active > .sf-sub-indicator, -.sf-vertical li:hover > a > .sf-sub-indicator, -.sf-vertical li.sfHover > a > .sf-sub-indicator { - background-position: -10px 0; /* arrow hovers for modern browsers*/ -} DELETED applications/welcome/static/css/superfish.css Index: applications/welcome/static/css/superfish.css ================================================================== --- applications/welcome/static/css/superfish.css +++ applications/welcome/static/css/superfish.css @@ -1,139 +0,0 @@ - -/*** ESSENTIAL STYLES ***/ -.sf-menu, .sf-menu * { - margin: 0; - padding: 0; - list-style: none; -} -.sf-menu { - line-height: 1.0; -} -.sf-menu ul { - position: absolute; - top: -999em; - width: 10em; /* left offset of submenus need to match (see below) */ -} -.sf-menu ul li { - width: 100%; -} -.sf-menu li:hover { - visibility: inherit; /* fixes IE7 'sticky bug' */ -} -.sf-menu li { - float: left; - position: relative; -} -.sf-menu a { - display: block; - position: relative; -} -.sf-menu li:hover ul, -.sf-menu li.sfHover ul { - left: 0; - top: 2.5em; /* match top ul list item height */ - z-index: 99; -} -ul.sf-menu li:hover li ul, -ul.sf-menu li.sfHover li ul { - top: -999em; -} -ul.sf-menu li li:hover ul, -ul.sf-menu li li.sfHover ul { - left: 10em; /* match ul width */ - top: 0; -} -ul.sf-menu li li:hover li ul, -ul.sf-menu li li.sfHover li ul { - top: -999em; -} -ul.sf-menu li li li:hover ul, -ul.sf-menu li li li.sfHover ul { - left: 10em; /* match ul width */ - top: 0; -} - -/*** DEMO SKIN ***/ -.sf-menu { - float: left; - /*margin-bottom: 1em;*/ -} -.sf-menu a { - border-left: 1px solid #fff; - /*border-top: 1px solid #CFDEFF;*/ - padding: .75em 1em; - text-decoration:none; -} -.sf-menu a, .sf-menu a:visited { /* visited pseudo selector so IE6 applies text colour*/ - color: #275b90;/*#13a;*/ -} -.sf-menu li { - background: #dadada;/*#BDD2FF;*/ -} -.sf-menu li li { - background: #AABDE6; -} -.sf-menu li li a { - /*color: #13a;*/ -} -.sf-menu li li li { - background: #9AAEDB; -} -.sf-menu li:hover, .sf-menu li.sfHover, -.sf-menu a:focus, .sf-menu a:hover, .sf-menu a:active { - background: #CFDEFF; - outline: 0; -} - -/*** arrows **/ -.sf-menu a.sf-with-ul { - padding-right: 2.25em; - min-width: 1px; /* trigger IE7 hasLayout so spans position accurately */ -} -.sf-sub-indicator { - position: absolute; - display: block; - right: .75em; - top: 1.05em; /* IE6 only */ - width: 10px; - height: 10px; - text-indent: -999em; - overflow: hidden; - background: url('../images/arrows-ffffff.png') no-repeat -10px -100px; /* 8-bit indexed alpha png. IE6 gets solid image only */ -} -a > .sf-sub-indicator { /* give all except IE6 the correct values */ - top: .8em; - background-position: 0 -100px; /* use translucent arrow for modern browsers*/ -} -/* apply hovers to modern browsers */ -a:focus > .sf-sub-indicator, -a:hover > .sf-sub-indicator, -a:active > .sf-sub-indicator, -li:hover > a > .sf-sub-indicator, -li.sfHover > a > .sf-sub-indicator { - background-position: -10px -100px; /* arrow hovers for modern browsers*/ -} - -/* point right for anchors in subs */ -.sf-menu ul .sf-sub-indicator { background-position: -10px 0; } -.sf-menu ul a > .sf-sub-indicator { background-position: 0 0; } -/* apply hovers to modern browsers */ -.sf-menu ul a:focus > .sf-sub-indicator, -.sf-menu ul a:hover > .sf-sub-indicator, -.sf-menu ul a:active > .sf-sub-indicator, -.sf-menu ul li:hover > a > .sf-sub-indicator, -.sf-menu ul li.sfHover > a > .sf-sub-indicator { - background-position: -10px 0; /* arrow hovers for modern browsers*/ -} - -/*** shadows for all but IE6 ***/ -.sf-shadow ul { - background: url('../images/shadow.png') no-repeat bottom right; - padding: 0 8px 9px 0; - -moz-border-radius-bottomleft: 17px; - -moz-border-radius-topright: 17px; - -webkit-border-top-right-radius: 17px; - -webkit-border-bottom-left-radius: 17px; -} -.sf-shadow ul.sf-shadow-off { - background: transparent; -} DELETED applications/welcome/static/favicon.ico Index: applications/welcome/static/favicon.ico ================================================================== --- applications/welcome/static/favicon.ico +++ applications/welcome/static/favicon.ico cannot compute difference between binary files DELETED applications/welcome/static/images/arrows-ffffff.png Index: applications/welcome/static/images/arrows-ffffff.png ================================================================== --- applications/welcome/static/images/arrows-ffffff.png +++ applications/welcome/static/images/arrows-ffffff.png cannot compute difference between binary files DELETED applications/welcome/static/images/css3buttons_backgrounds.png Index: applications/welcome/static/images/css3buttons_backgrounds.png ================================================================== --- applications/welcome/static/images/css3buttons_backgrounds.png +++ applications/welcome/static/images/css3buttons_backgrounds.png cannot compute difference between binary files DELETED applications/welcome/static/images/css3buttons_icons.png Index: applications/welcome/static/images/css3buttons_icons.png ================================================================== --- applications/welcome/static/images/css3buttons_icons.png +++ applications/welcome/static/images/css3buttons_icons.png cannot compute difference between binary files DELETED applications/welcome/static/images/error.png Index: applications/welcome/static/images/error.png ================================================================== --- applications/welcome/static/images/error.png +++ applications/welcome/static/images/error.png cannot compute difference between binary files DELETED applications/welcome/static/images/ok.png Index: applications/welcome/static/images/ok.png ================================================================== --- applications/welcome/static/images/ok.png +++ applications/welcome/static/images/ok.png cannot compute difference between binary files DELETED applications/welcome/static/images/poweredby.png Index: applications/welcome/static/images/poweredby.png ================================================================== --- applications/welcome/static/images/poweredby.png +++ applications/welcome/static/images/poweredby.png cannot compute difference between binary files DELETED applications/welcome/static/images/shadow.png Index: applications/welcome/static/images/shadow.png ================================================================== --- applications/welcome/static/images/shadow.png +++ applications/welcome/static/images/shadow.png cannot compute difference between binary files DELETED applications/welcome/static/images/warn.png Index: applications/welcome/static/images/warn.png ================================================================== --- applications/welcome/static/images/warn.png +++ applications/welcome/static/images/warn.png cannot compute difference between binary files DELETED applications/welcome/static/images/warning.png Index: applications/welcome/static/images/warning.png ================================================================== --- applications/welcome/static/images/warning.png +++ applications/welcome/static/images/warning.png cannot compute difference between binary files DELETED applications/welcome/static/js/calendar.js Index: applications/welcome/static/js/calendar.js ================================================================== --- applications/welcome/static/js/calendar.js +++ applications/welcome/static/js/calendar.js cannot compute difference between binary files DELETED applications/welcome/static/js/dd_belatedpng.js Index: applications/welcome/static/js/dd_belatedpng.js ================================================================== --- applications/welcome/static/js/dd_belatedpng.js +++ applications/welcome/static/js/dd_belatedpng.js @@ -1,13 +0,0 @@ -/** -* DD_belatedPNG: Adds IE6 support: PNG images for CSS background-image and HTML . -* Author: Drew Diller -* Email: drew.diller@gmail.com -* URL: http://www.dillerdesign.com/experiment/DD_belatedPNG/ -* Version: 0.0.8a -* Licensed under the MIT License: http://dillerdesign.com/experiment/DD_belatedPNG/#license -* -* Example usage: -* DD_belatedPNG.fix('.png_bg'); // argument is a CSS selector -* DD_belatedPNG.fixPng( someNode ); // argument is an HTMLDomElement -**/ -var DD_belatedPNG={ns:"DD_belatedPNG",imgSize:{},delay:10,nodesFixed:0,createVmlNameSpace:function(){if(document.namespaces&&!document.namespaces[this.ns]){document.namespaces.add(this.ns,"urn:schemas-microsoft-com:vml")}},createVmlStyleSheet:function(){var b,a;b=document.createElement("style");b.setAttribute("media","screen");document.documentElement.firstChild.insertBefore(b,document.documentElement.firstChild.firstChild);if(b.styleSheet){b=b.styleSheet;b.addRule(this.ns+"\\:*","{behavior:url(#default#VML)}");b.addRule(this.ns+"\\:shape","position:absolute;");b.addRule("img."+this.ns+"_sizeFinder","behavior:none; border:none; position:absolute; z-index:-1; top:-10000px; visibility:hidden;");this.screenStyleSheet=b;a=document.createElement("style");a.setAttribute("media","print");document.documentElement.firstChild.insertBefore(a,document.documentElement.firstChild.firstChild);a=a.styleSheet;a.addRule(this.ns+"\\:*","{display: none !important;}");a.addRule("img."+this.ns+"_sizeFinder","{display: none !important;}")}},readPropertyChange:function(){var b,c,a;b=event.srcElement;if(!b.vmlInitiated){return}if(event.propertyName.search("background")!=-1||event.propertyName.search("border")!=-1){DD_belatedPNG.applyVML(b)}if(event.propertyName=="style.display"){c=(b.currentStyle.display=="none")?"none":"block";for(a in b.vml){if(b.vml.hasOwnProperty(a)){b.vml[a].shape.style.display=c}}}if(event.propertyName.search("filter")!=-1){DD_belatedPNG.vmlOpacity(b)}},vmlOpacity:function(b){if(b.currentStyle.filter.search("lpha")!=-1){var a=b.currentStyle.filter;a=parseInt(a.substring(a.lastIndexOf("=")+1,a.lastIndexOf(")")),10)/100;b.vml.color.shape.style.filter=b.currentStyle.filter;b.vml.image.fill.opacity=a}},handlePseudoHover:function(a){setTimeout(function(){DD_belatedPNG.applyVML(a)},1)},fix:function(a){if(this.screenStyleSheet){var c,b;c=a.split(",");for(b=0;bn.H){i.B=n.H}d.vml.image.shape.style.clip="rect("+i.T+"px "+(i.R+a)+"px "+i.B+"px "+(i.L+a)+"px)"}else{d.vml.image.shape.style.clip="rect("+f.T+"px "+f.R+"px "+f.B+"px "+f.L+"px)"}},figurePercentage:function(d,c,f,a){var b,e;e=true;b=(f=="X");switch(a){case"left":case"top":d[f]=0;break;case"center":d[f]=0.5;break;case"right":case"bottom":d[f]=1;break;default:if(a.search("%")!=-1){d[f]=parseInt(a,10)/100}else{e=false}}d[f]=Math.ceil(e?((c[b?"W":"H"]*d[f])-(c[b?"w":"h"]*d[f])):parseInt(a,10));if(d[f]%2===0){d[f]++}return d[f]},fixPng:function(c){c.style.behavior="none";var g,b,f,a,d;if(c.nodeName=="BODY"||c.nodeName=="TD"||c.nodeName=="TR"){return}c.isImg=false;if(c.nodeName=="IMG"){if(c.src.toLowerCase().search(/\.png$/)!=-1){c.isImg=true;c.style.visibility="hidden"}else{return}}else{if(c.currentStyle.backgroundImage.toLowerCase().search(".png")==-1){return}}g=DD_belatedPNG;c.vml={color:{},image:{}};b={shape:{},fill:{}};for(a in c.vml){if(c.vml.hasOwnProperty(a)){for(d in b){if(b.hasOwnProperty(d)){f=g.ns+":"+d;c.vml[a][d]=document.createElement(f)}}c.vml[a].shape.stroked=false;c.vml[a].shape.appendChild(c.vml[a].fill);c.parentNode.insertBefore(c.vml[a].shape,c)}}c.vml.image.shape.fillcolor="none";c.vml.image.fill.type="tile";c.vml.color.fill.on=false;g.attachHandlers(c);g.giveLayout(c);g.giveLayout(c.offsetParent);c.vmlInitiated=true;g.applyVML(c)}};try{document.execCommand("BackgroundImageCache",false,true)}catch(r){}DD_belatedPNG.createVmlNameSpace();DD_belatedPNG.createVmlStyleSheet(); DELETED applications/welcome/static/js/jquery.js Index: applications/welcome/static/js/jquery.js ================================================================== --- applications/welcome/static/js/jquery.js +++ applications/welcome/static/js/jquery.js cannot compute difference between binary files DELETED applications/welcome/static/js/modernizr-1.7.min.js Index: applications/welcome/static/js/modernizr-1.7.min.js ================================================================== --- applications/welcome/static/js/modernizr-1.7.min.js +++ applications/welcome/static/js/modernizr-1.7.min.js @@ -1,2 +0,0 @@ -// Modernizr v1.7 www.modernizr.com -window.Modernizr=function(a,b,c){function G(){e.input=function(a){for(var b=0,c=a.length;b7)},r.history=function(){return !!(a.history&&history.pushState)},r.draganddrop=function(){return x("dragstart")&&x("drop")},r.websockets=function(){return"WebSocket"in a},r.rgba=function(){A("background-color:rgba(150,255,150,.5)");return D(k.backgroundColor,"rgba")},r.hsla=function(){A("background-color:hsla(120,40%,100%,.5)");return D(k.backgroundColor,"rgba")||D(k.backgroundColor,"hsla")},r.multiplebgs=function(){A("background:url(//:),url(//:),red url(//:)");return(new RegExp("(url\\s*\\(.*?){3}")).test(k.background)},r.backgroundsize=function(){return F("backgroundSize")},r.borderimage=function(){return F("borderImage")},r.borderradius=function(){return F("borderRadius","",function(a){return D(a,"orderRadius")})},r.boxshadow=function(){return F("boxShadow")},r.textshadow=function(){return b.createElement("div").style.textShadow===""},r.opacity=function(){B("opacity:.55");return/^0.55$/.test(k.opacity)},r.cssanimations=function(){return F("animationName")},r.csscolumns=function(){return F("columnCount")},r.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";A((a+o.join(b+a)+o.join(c+a)).slice(0,-a.length));return D(k.backgroundImage,"gradient")},r.cssreflections=function(){return F("boxReflect")},r.csstransforms=function(){return!!E(["transformProperty","WebkitTransform","MozTransform","OTransform","msTransform"])},r.csstransforms3d=function(){var a=!!E(["perspectiveProperty","WebkitPerspective","MozPerspective","OPerspective","msPerspective"]);a&&"webkitPerspective"in g.style&&(a=w("@media ("+o.join("transform-3d),(")+"modernizr)"));return a},r.csstransitions=function(){return F("transitionProperty")},r.fontface=function(){var a,c,d=h||g,e=b.createElement("style"),f=b.implementation||{hasFeature:function(){return!1}};e.type="text/css",d.insertBefore(e,d.firstChild),a=e.sheet||e.styleSheet;var i=f.hasFeature("CSS2","")?function(b){if(!a||!b)return!1;var c=!1;try{a.insertRule(b,0),c=/src/i.test(a.cssRules[0].cssText),a.deleteRule(a.cssRules.length-1)}catch(d){}return c}:function(b){if(!a||!b)return!1;a.cssText=b;return a.cssText.length!==0&&/src/i.test(a.cssText)&&a.cssText.replace(/\r+|\n+/g,"").indexOf(b.split(" ")[0])===0};c=i('@font-face { font-family: "font"; src: url(data:,); }'),d.removeChild(e);return c},r.video=function(){var a=b.createElement("video"),c=!!a.canPlayType;if(c){c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"');var d='video/mp4; codecs="avc1.42E01E';c.h264=a.canPlayType(d+'"')||a.canPlayType(d+', mp4a.40.2"'),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"')}return c},r.audio=function(){var a=b.createElement("audio"),c=!!a.canPlayType;c&&(c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"'),c.mp3=a.canPlayType("audio/mpeg;"),c.wav=a.canPlayType('audio/wav; codecs="1"'),c.m4a=a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;"));return c},r.localstorage=function(){try{return!!localStorage.getItem}catch(a){return!1}},r.sessionstorage=function(){try{return!!sessionStorage.getItem}catch(a){return!1}},r.webWorkers=function(){return!!a.Worker},r.applicationcache=function(){return!!a.applicationCache},r.svg=function(){return!!b.createElementNS&&!!b.createElementNS(q.svg,"svg").createSVGRect},r.inlinesvg=function(){var a=b.createElement("div");a.innerHTML="";return(a.firstChild&&a.firstChild.namespaceURI)==q.svg},r.smil=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"animate")))},r.svgclippaths=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"clipPath")))};for(var H in r)z(r,H)&&(v=H.toLowerCase(),e[v]=r[H](),u.push((e[v]?"":"no-")+v));e.input||G(),e.crosswindowmessaging=e.postmessage,e.historymanagement=e.history,e.addTest=function(a,b){a=a.toLowerCase();if(!e[a]){b=!!b(),g.className+=" "+(b?"":"no-")+a,e[a]=b;return e}},A(""),j=l=null,f&&a.attachEvent&&function(){var a=b.createElement("div");a.innerHTML="";return a.childNodes.length!==1}()&&function(a,b){function p(a,b){var c=-1,d=a.length,e,f=[];while(++c »'].join('')), - over = function(){ - var $$ = $(this), menu = getMenu($$); - clearTimeout(menu.sfTimer); - $$.showSuperfishUl().siblings().hideSuperfishUl(); - }, - out = function(){ - var $$ = $(this), menu = getMenu($$), o = sf.op; - clearTimeout(menu.sfTimer); - menu.sfTimer=setTimeout(function(){ - o.retainPath=($.inArray($$[0],o.$path)>-1); - $$.hideSuperfishUl(); - if (o.$path.length && $$.parents(['li.',o.hoverClass].join('')).length<1){over.call(o.$path);} - },o.delay); - }, - getMenu = function($menu){ - var menu = $menu.parents(['ul.',c.menuClass,':first'].join(''))[0]; - sf.op = sf.o[menu.serial]; - return menu; - }, - addArrow = function($a){ $a.addClass(c.anchorClass).append($arrow.clone()); }; - - return this.each(function() { - var s = this.serial = sf.o.length; - var o = $.extend({},sf.defaults,op); - o.$path = $('li.'+o.pathClass,this).slice(0,o.pathLevels).each(function(){ - $(this).addClass([o.hoverClass,c.bcClass].join(' ')) - .filter('li:has(ul)').removeClass(o.pathClass); - }); - sf.o[s] = sf.op = o; - - $('li:has(ul)',this)[($.fn.hoverIntent && !o.disableHI) ? 'hoverIntent' : 'hover'](over,out).each(function() { - if (o.autoArrows) addArrow( $('>a:first-child',this) ); - }) - .not('.'+c.bcClass) - .hideSuperfishUl(); - - var $a = $('a',this); - $a.each(function(i){ - var $li = $a.eq(i).parents('li'); - $a.eq(i).focus(function(){over.call($li);}).blur(function(){out.call($li);}); - }); - o.onInit.call(this); - - }).each(function() { - var menuClasses = [c.menuClass]; - if (sf.op.dropShadows && !($.browser.msie && $.browser.version < 7)) menuClasses.push(c.shadowClass); - $(this).addClass(menuClasses.join(' ')); - }); - }; - - var sf = $.fn.superfish; - sf.o = []; - sf.op = {}; - sf.IE7fix = function(){ - var o = sf.op; - if ($.browser.msie && $.browser.version > 6 && o.dropShadows && o.animation.opacity!=undefined) - this.toggleClass(sf.c.shadowClass+'-off'); - }; - sf.c = { - bcClass : 'sf-breadcrumb', - menuClass : 'sf-js-enabled', - anchorClass : 'sf-with-ul', - arrowClass : 'sf-sub-indicator', - shadowClass : 'sf-shadow' - }; - sf.defaults = { - hoverClass : 'sfHover', - pathClass : 'overideThisToUse', - pathLevels : 1, - delay : 800, - animation : {opacity:'show'}, - speed : 'normal', - autoArrows : true, - dropShadows : true, - disableHI : false, // true disables hoverIntent detection - onInit : function(){}, // callback functions - onBeforeShow: function(){}, - onShow : function(){}, - onHide : function(){} - }; - $.fn.extend({ - hideSuperfishUl : function(){ - var o = sf.op, - not = (o.retainPath===true) ? o.$path : ''; - o.retainPath = false; - var $ul = $(['li.',o.hoverClass].join(''),this).add(this).not(not).removeClass(o.hoverClass) - .find('>ul').hide().css('visibility','hidden'); - o.onHide.call($ul); - return this; - }, - showSuperfishUl : function(){ - var o = sf.op, - sh = sf.c.shadowClass+'-off', - $ul = this.addClass(o.hoverClass) - .find('>ul:hidden').css('visibility','visible'); - sf.IE7fix.call($ul); - o.onBeforeShow.call($ul); - $ul.animate(o.animation,o.speed,function(){ sf.IE7fix.call($ul); o.onShow.call($ul); }); - return this; - } - }); - -})(jQuery); DELETED applications/welcome/static/js/web2py_ajax.js Index: applications/welcome/static/js/web2py_ajax.js ================================================================== --- applications/welcome/static/js/web2py_ajax.js +++ applications/welcome/static/js/web2py_ajax.js @@ -1,97 +0,0 @@ -function popup(url) { - newwindow=window.open(url,'name','height=400,width=600'); - if (window.focus) newwindow.focus(); - return false; -} -function collapse(id) { jQuery('#'+id).slideToggle(); } -function fade(id,value) { if(value>0) jQuery('#'+id).hide().fadeIn('slow'); else jQuery('#'+id).show().fadeOut('slow'); } -function ajax(u,s,t) { - query = ''; - if (typeof s == "string") { - d = jQuery(s).serialize(); - if(d){ query = d; } - } else { - pcs = []; - for(i=0; i0){query = pcs.join("&");} - } - jQuery.ajax({type: "POST", url: u, data: query, success: function(msg) { if(t) { if(t==':eval') eval(msg); else jQuery("#" + t).html(msg); } } }); -} - -String.prototype.reverse = function () { return this.split('').reverse().join('');}; -function web2py_ajax_init() { - jQuery('.hidden').hide(); - jQuery('.error').hide().slideDown('slow'); - jQuery('.flash').click(function() { jQuery(this).fadeOut('slow'); return false; }); - // jQuery('input[type=submit]').click(function(){var t=jQuery(this);t.hide();t.after('')}); - jQuery('input.integer').live('keyup', function(){this.value=this.value.reverse().replace(/[^0-9\-]|\-(?=.)/g,'').reverse();}); - jQuery('input.double,input.decimal').live('keyup', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();}); - var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?"; - jQuery("input[type='checkbox'].delete").live('click', function(){ if(this.checked) if(!confirm(confirm_message)) this.checked=false; }); - var date_format = (typeof w2p_ajax_date_format != 'undefined') ? w2p_ajax_date_format : "%Y-%m-%d"; - try {jQuery("input.date").live('focus',function() {Calendar.setup({ - inputField:this, ifFormat:date_format, showsTime:false - }); }); } catch(e) {}; - var datetime_format = (typeof w2p_ajax_datetime_format != 'undefined') ? w2p_ajax_datetime_format : "%Y-%m-%d %H:%M:%S"; - try { jQuery("input.datetime").live('focus', function() {Calendar.setup({ - inputField:this, ifFormat:datetime_format, showsTime: true,timeFormat: "24" - }); }); } catch(e) {}; - - jQuery("input.time").live('focus', function() { var el = jQuery(this); - if (!el.hasClass('hasTimeEntry')) try { el.timeEntry(); } catch(e) {}; - }); -}; - -jQuery(function() { - var flash = jQuery('.flash'); - flash.hide(); - if(flash.html()) flash.slideDown(); - web2py_ajax_init(); -}); -function web2py_trap_form(action,target) { - jQuery('#'+target+' form').each(function(i){ - var form=jQuery(this); - if(!form.hasClass('no_trap')) - form.submit(function(obj){ - jQuery('.flash').hide().html(''); - web2py_ajax_page('post',action,form.serialize(),target); - return false; - }); - }); -} -function web2py_ajax_page(method,action,data,target) { - jQuery.ajax({'type':method,'url':action,'data':data, - 'beforeSend':function(xhr) { - xhr.setRequestHeader('web2py-component-location',document.location); - xhr.setRequestHeader('web2py-component-element',target);}, - 'complete':function(xhr,text){ - var html=xhr.responseText; - var content=xhr.getResponseHeader('web2py-component-content'); - var command=xhr.getResponseHeader('web2py-component-command'); - var flash=xhr.getResponseHeader('web2py-component-flash'); - var t = jQuery('#'+target); - if(content=='prepend') t.prepend(html); - else if(content=='append') t.append(html); - else if(content!='hide') t.html(html); - web2py_trap_form(action,target); - web2py_ajax_init(); - if(command) eval(command); - if(flash) jQuery('.flash').html(flash).slideDown(); - } - }); -} -function web2py_component(action,target) { - jQuery(function(){ web2py_ajax_page('get',action,null,target); }); -} -function web2py_comet(url,onmessage,onopen,onclose) { - if ("WebSocket" in window) { - var ws = new WebSocket(url); - ws.onopen = onopen?onopen:(function(){}); - ws.onmessage = onmessage; - ws.onclose = onclose?onclose:(function(){}); - return true; // supported - } else return false; // not supported -} DELETED applications/welcome/static/robots.txt Index: applications/welcome/static/robots.txt ================================================================== --- applications/welcome/static/robots.txt +++ applications/welcome/static/robots.txt @@ -1,2 +0,0 @@ -User-agent: * -Disallow: /welcome/default/user DELETED applications/welcome/views/__init__.py Index: applications/welcome/views/__init__.py ================================================================== --- applications/welcome/views/__init__.py +++ applications/welcome/views/__init__.py DELETED applications/welcome/views/appadmin.html Index: applications/welcome/views/appadmin.html ================================================================== --- applications/welcome/views/appadmin.html +++ applications/welcome/views/appadmin.html @@ -1,198 +0,0 @@ -{{extend 'layout.html'}} - - -{{if request.function=='index':}} -

    {{=T("Available databases and tables")}}

    - {{if not databases:}}{{=T("No databases in this application")}}{{pass}} - {{for db in sorted(databases):}} - {{for table in databases[db].tables:}} - {{qry='%s.%s.id>0'%(db,table)}} - {{tbl=databases[db][table]}} - {{if hasattr(tbl,'_primarykey'):}} - {{if tbl._primarykey:}} - {{firstkey=tbl[tbl._primarykey[0]]}} - {{if firstkey.type in ['string','text']:}} - {{qry='%s.%s.%s!=""'%(db,table,firstkey.name)}} - {{else:}} - {{qry='%s.%s.%s>0'%(db,table,firstkey.name)}} - {{pass}} - {{else:}} - {{qry=''}} - {{pass}} - {{pass}} -

    {{=A("%s.%s" % (db,table),_href=URL('select',args=[db],vars=dict(query=qry)))}} -

    - [ {{=A(str(T('insert new'))+' '+table,_href=URL('insert',args=[db,table]))}} ] -

    - {{pass}} - {{pass}} - -{{elif request.function=='select':}} -

    {{=XML(str(T("database %s select"))%A(request.args[0],_href=URL('index'))) }} -

    - {{if table:}} - [ {{=A(str(T('insert new %s'))%table,_href=URL('insert',args=[request.args[0],table]))}} ]

    -

    {{=T("Rows in table")}}


    - {{else:}} -

    {{=T("Rows selected")}}


    - {{pass}} - {{=form}} -

    {{=T('The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.')}}
    - {{=T('Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.')}}
    - {{=T('"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN')}}

    -

    -

    {{=nrows}} {{=T("selected")}}

    - {{if start>0:}}[ {{=A(T('previous 100 rows'),_href=URL('select',args=request.args[0],vars=dict(start=start-100)))}} ]{{pass}} - {{if stop - {{linkto=URL('update',args=request.args[0])}} - {{upload=URL('download',args=request.args[0])}} - {{=SQLTABLE(rows,linkto,upload,orderby=True,_class='sortable')}} - - {{pass}} -

    {{=T("Import/Export")}}


    - [ {{=T("export as csv file")}} ] - {{if table:}} - {{=FORM(str(T('or import from csv file'))+" ",INPUT(_type='file',_name='csvfile'),INPUT(_type='hidden',_value=table,_name='table'),INPUT(_type='submit',_value='import'))}} - {{pass}} - - -{{elif request.function=='insert':}} -

    {{=T("database")}} {{=A(request.args[0],_href=URL('index'))}} - {{if hasattr(table,'_primarykey'):}} - {{fieldname=table._primarykey[0]}} - {{dbname=request.args[0]}} - {{tablename=request.args[1]}} - {{cond = table[fieldname].type in ['string','text'] and '!=""' or '>0'}} - {{=T("table")}} {{=A(tablename,_href=URL('select',args=dbname,vars=dict(query='%s.%s.%s%s'%(dbname,tablename,fieldname,cond))))}} - {{else:}} - {{=T("table")}} {{=A(request.args[1],_href=URL('select',args=request.args[0],vars=dict(query='%s.%s.id>0'%tuple(request.args[:2]))))}} - {{pass}} -

    -

    {{=T("New Record")}}


    - {{=form}} - - - -{{elif request.function=='update':}} -

    {{=T("database")}} {{=A(request.args[0],_href=URL('index'))}} - {{if hasattr(table,'_primarykey'):}} - {{fieldname=request.vars.keys()[0]}} - {{dbname=request.args[0]}} - {{tablename=request.args[1]}} - {{cond = table[fieldname].type in ['string','text'] and '!=""' or '>0'}} - {{=T("table")}} {{=A(tablename,_href=URL('select',args=dbname,vars=dict(query='%s.%s.%s%s'%(dbname,tablename,fieldname,cond))))}} - {{=T("record")}} {{=A('%s=%s'%request.vars.items()[0],_href=URL('update',args=request.args[:2],vars=request.vars))}} - {{else:}} - {{=T("table")}} {{=A(request.args[1],_href=URL('select',args=request.args[0],vars=dict(query='%s.%s.id>0'%tuple(request.args[:2]))))}} - {{=T("record id")}} {{=A(request.args[2],_href=URL('update',args=request.args[:3]))}} - {{pass}} -

    -

    {{=T("Edit current record")}}



    {{=form}} - - - -{{elif request.function=='state':}} -

    {{=T("Internal State")}}

    -

    {{=T("Current request")}}

    - {{=BEAUTIFY(request)}} -

    {{=T("Current response")}}

    - {{=BEAUTIFY(response)}} -

    {{=T("Current session")}}

    - {{=BEAUTIFY(session)}} - - -{{elif request.function == 'ccache':}} -

    Cache

    -
    -
    -
    - Statistics -
    -
    -

    Overview

    -

    - Hit Ratio: - {{=total['ratio']}}% - ({{=total['hits']}} hits - and {{=total['misses']}} misses) -

    -

    - Size of cache: - {{=total['objects']}} items, - {{=total['bytes']}} bytes - {{if total['bytes'] > 524287:}} - ({{="%.0d" % (total['bytes'] / 1048576)}} MB) - {{pass}} -

    -

    - Cache contains items up to - {{="%02d" % total['oldest'][0]}} hours - {{="%02d" % total['oldest'][1]}} minutes - {{="%02d" % total['oldest'][2]}} seconds old. -

    -

    RAM

    -

    - Hit Ratio: - {{=ram['ratio']}}% - ({{=ram['hits']}} hits - and {{=ram['misses']}} misses) -

    -

    - Size of cache: - {{=ram['objects']}} items, - {{=ram['bytes']}} bytes - {{if ram['bytes'] > 524287:}} - ({{=ram['bytes'] / 1048576}} MB) - {{pass}} -

    -

    - RAM contains items up to - {{="%02d" % ram['oldest'][0]}} hours - {{="%02d" % ram['oldest'][1]}} minutes - {{="%02d" % ram['oldest'][2]}} seconds old. -

    -

    DISK

    -

    - Hit Ratio: - {{=disk['ratio']}}% - ({{=disk['hits']}} hits - and {{=disk['misses']}} misses) -

    -

    - Size of cache: - {{=disk['objects']}} items, - {{=disk['bytes']}} bytes - {{if disk['bytes'] > 524287:}} - ({{=disk['bytes'] / 1048576}} MB) - {{pass}} -

    -

    - DISK contains items up to - {{="%02d" % disk['oldest'][0]}} hours - {{="%02d" % disk['oldest'][1]}} minutes - {{="%02d" % disk['oldest'][2]}} seconds old. -

    -
    - -
    - Manage Cache -
    -
    -

    - {{=form}} -

    -
    -
    -
    -
    -{{pass}} DELETED applications/welcome/views/default/index.html Index: applications/welcome/views/default/index.html ================================================================== --- applications/welcome/views/default/index.html +++ applications/welcome/views/default/index.html @@ -1,33 +0,0 @@ -{{left_sidebar_enabled=right_sidebar_enabled=False}} -{{extend 'layout.html'}} - -{{if 'message' in globals():}} - -

    {{=message}}

    - -
    -

    {{=T("Readme")}}

    -
      -
    • {{=T('You are successfully running web2py')}}
    • -
    • {{=T('This is a copy of the scaffolding application')}}
    • -
    • {{=T('You can modify this application and adapt it to your needs')}}
    • -
    • {{=A(B(T("Administrative interface")), _href=URL('admin','default','index'))}}
    • -
    • {{=A(T("Online examples"), _href=URL('examples','default','index'))}}
    • -
    • web2py.com
    • -
    • {{=T('Documentation')}}
    • -
    - -
      -
    1. {{=T('You visited the url')}} {{=A(request.env.path_info,_href=request.env.path_info)}}
    2. -
    3. {{=T('Which called the function')}} {{=A(request.function+'()',_href='#')}} {{=T('located in the file')}} - {{=A('web2py/applications/%(application)s/controllers/%(controller)s.py'%request,_href=URL('admin','default','peek',args=(request.application,'controllers',request.controller+'.py')))}}
    4. -
    5. {{=T('The output of the file is a dictionary that was rendered by the view')}} - {{=A('web2py/applications/%(application)s/views/%(controller)s/index.html'%request,_href=URL('admin','default','peek',args=(request.application,'views',request.controller,'index.html')))}}
    6. -
    - -{{else:}} -{{=BEAUTIFY(response._vars)}} -{{pass}} - -{{block left_sidebar}}New Left Sidebar Content{{end}} -{{block right_sidebar}}New Right Sidebar Content{{end}} DELETED applications/welcome/views/default/user.html Index: applications/welcome/views/default/user.html ================================================================== --- applications/welcome/views/default/user.html +++ applications/welcome/views/default/user.html @@ -1,19 +0,0 @@ -{{extend 'layout.html'}} -

    {{=T( request.args(0).replace('_',' ').capitalize() )}}

    -
    -{{=form}} -{{if request.args(0)=='login':}} -{{if not 'register' in auth.settings.actions_disabled:}} -
    register -{{pass}} -{{if not 'request_reset_password' in auth.settings.actions_disabled:}} -
    lost password -{{pass}} -{{pass}} -
    - - DELETED applications/welcome/views/generic.html Index: applications/welcome/views/generic.html ================================================================== --- applications/welcome/views/generic.html +++ applications/welcome/views/generic.html @@ -1,16 +0,0 @@ -{{extend 'layout.html'}} -{{""" - -You should not modify this file. -It is used as default when a view is not provided for your controllers - -"""}} -

    {{=' '.join(x.capitalize() for x in request.function.split('_'))}}

    -{{if len(response._vars)==1:}} -{{=response._vars.values()[0]}} -{{elif len(response._vars)>1:}} -{{=BEAUTIFY(response._vars)}} -{{pass}} -{{if request.is_local:}} -{{=response.toolbar()}} -{{pass}} DELETED applications/welcome/views/generic.json Index: applications/welcome/views/generic.json ================================================================== --- applications/welcome/views/generic.json +++ applications/welcome/views/generic.json @@ -1,1 +0,0 @@ -{{from gluon.serializers import json}}{{=XML(json(response._vars))}} DELETED applications/welcome/views/generic.load Index: applications/welcome/views/generic.load ================================================================== --- applications/welcome/views/generic.load +++ applications/welcome/views/generic.load @@ -1,30 +0,0 @@ -{{''' -# License: Public Domain -# Author: Iceberg at 21cn dot com - -With this generic.load file, you can use same function to serve two purposes. - -= regular action -- ajax callback (when called with .load) - -Example modified from http://www.web2py.com/AlterEgo/default/show/252: - -def index(): - return dict( - part1='hello world', - part2=LOAD(url=URL(r=request,f='auxiliary.load'),ajax=True)) - -def auxiliary(): - form=SQLFORM.factory(Field('name')) - if form.accepts(request.vars): - response.flash = 'ok' - return dict(message="Hello %s" % form.vars.name) - return dict(form=form) - -Notice: - -- no need to set response.headers['web2py-response-flash'] -- no need to return a string -even if the function is called via ajax. - -'''}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}} DELETED applications/welcome/views/generic.pdf Index: applications/welcome/views/generic.pdf ================================================================== --- applications/welcome/views/generic.pdf +++ applications/welcome/views/generic.pdf @@ -1,11 +0,0 @@ -{{ -import os -from gluon.contrib.generics import pdf_from_html -filename = '%s/%s.html' % (request.controller,request.function) -if os.path.exists(os.path.join(request.folder,'views',filename)): - html=response.render(filename) -else: - html=BODY(BEAUTIFY(response._vars)).xml() -pass -=pdf_from_html(html) -}} DELETED applications/welcome/views/generic.rss Index: applications/welcome/views/generic.rss ================================================================== --- applications/welcome/views/generic.rss +++ applications/welcome/views/generic.rss @@ -1,10 +0,0 @@ -{{ -### -# response._vars contains the dictionary returned by the controller action -# for this to work the action must return something like -# -# dict(title=...,link=...,description=...,created_on='...',items=...) -# -# items is a list of dictionaries each with title, link, description, pub_date. -### -from gluon.serializers import rss}}{{=XML(rss(response._vars))}} DELETED applications/welcome/views/generic.xml Index: applications/welcome/views/generic.xml ================================================================== --- applications/welcome/views/generic.xml +++ applications/welcome/views/generic.xml @@ -1,1 +0,0 @@ -{{from gluon.serializers import xml}}{{=XML(xml(response._vars))}} DELETED applications/welcome/views/layout.html Index: applications/welcome/views/layout.html ================================================================== --- applications/welcome/views/layout.html +++ applications/welcome/views/layout.html @@ -1,159 +0,0 @@ - - - - - - - - - - - {{=response.title or request.application}} - - - - - - - - - - - - - - - - - - - - - {{#------ require CSS and JS files for this page (read info in base.css) ------}} - {{response.files.append(URL('static','css/base.css'))}} - {{response.files.append(URL('static','css/superfish.css'))}} - {{response.files.append(URL('static','js/superfish.js'))}} - {{#------ include web2py specific js code (jquery, calendar, form stuff) ------}} - {{include 'web2py_ajax.html'}} - - {{ - #using sidebars need to know what sidebar you want to use - #prior of using it, because of static width size of content, you can use - #left_sidebar, right_sidebar, both or none (False left and right) - left_sidebar_enabled = globals().get('left_sidebar_enabled',False) - right_sidebar_enabled = globals().get('right_sidebar_enabled',False) - if left_sidebar_enabled and right_sidebar_enabled: width_content='63%' - elif left_sidebar_enabled != right_sidebar_enabled: width_content='740px' - else: width_content='100%' - if left_sidebar_enabled: left_sidebar_style = 'style="display: block;"' - else: left_sidebar_style = 'style="display: none;"' - if right_sidebar_enabled: right_sidebar_style = 'style="display: block;"' - else: right_sidebar_style = 'style="display: none;"' - style_content = 'style="width: %s"' % width_content - }} - - - - - - - - - -
    {{=response.flash or ''}}
    - -
    - -
    - - - -
    - {{block statusbar}} - {{#------ superfish menu ------}} - {{=MENU(response.menu,_class='sf-menu')}} - -
    - {{end}} -
    - -
    - - {{if left_sidebar_enabled:}} - - {{pass}} - - -
    - {{include}} -
    - - - {{if right_sidebar_enabled:}} - - {{pass}} - - -
    - -
    - - -
    -
    - - - - - - - DELETED applications/welcome/views/web2py_ajax.html Index: applications/welcome/views/web2py_ajax.html ================================================================== --- applications/welcome/views/web2py_ajax.html +++ applications/welcome/views/web2py_ajax.html @@ -1,25 +0,0 @@ -{{ -response.files.insert(0,URL('static','js/jquery.js')) -response.files.insert(1,URL('static','css/calendar.css')) -response.files.insert(2,URL('static','js/calendar.js')) -for _item in response.meta or []:}} - {{ -pass -for _k,_file in enumerate(response.files or []): - if _file in response.files[:_k]: - continue - _file0=_file.lower().split('?')[0] - if _file0.endswith('.css'):}} - {{ - elif _file0.endswith('.js'):}} - {{ - pass -pass -}} - - Index: cgihandler.py ================================================================== --- cgihandler.py +++ cgihandler.py @@ -16,5 +16,6 @@ sys.path = [path]+[p for p in sys.path if not p==path] import gluon.main wsgiref.handlers.CGIHandler().run(gluon.main.wsgibase) + Index: fcgihandler.py ================================================================== --- fcgihandler.py +++ fcgihandler.py @@ -49,5 +49,6 @@ if SOFTCRON: from gluon.settings import global_settings global_settings.web2py_crontype = 'soft' fcgi.WSGIServer(application, bindAddress='/tmp/fcgi.sock').run() + Index: gaehandler.py ================================================================== --- gaehandler.py +++ gaehandler.py @@ -111,5 +111,6 @@ else: wsgiref.handlers.CGIHandler().run(wsgiapp) if __name__ == '__main__': main() + Index: gluon/__init__.py ================================================================== --- gluon/__init__.py +++ gluon/__init__.py @@ -16,6 +16,10 @@ from html import * from validators import * from http import redirect, HTTP from dal import DAL, Field from sqlhtml import SQLFORM, SQLTABLE + + + + ADDED gluon/__init__.pyc Index: gluon/__init__.pyc ================================================================== --- gluon/__init__.pyc +++ gluon/__init__.pyc cannot compute difference between binary files Index: gluon/admin.py ================================================================== --- gluon/admin.py +++ gluon/admin.py @@ -13,11 +13,11 @@ import urllib from shutil import rmtree from utils import web2py_uuid from fileutils import w2p_pack, w2p_unpack, w2p_pack_plugin, w2p_unpack_plugin from fileutils import up, fix_newlines, abspath, recursive_unlink -from fileutils import read_file, write_file +from fileutils import read_file, write_file, parse_version from restricted import RestrictedError from settings import global_settings def apath(path='', r=None): """ @@ -342,11 +342,11 @@ version: the most up-to-version available """ try: from urllib import urlopen - version = urlopen(version_URL).read() + version = parse_version(urlopen(version_URL).read()) except Exception: return -1, myversion if version > myversion: return True, version @@ -448,6 +448,8 @@ 'languages', 'static', 'private', 'uploads'): path = os.path.join(request.folder, subfolder) if not os.path.exists(path): os.mkdir(path) global_settings.app_folders.add(request.folder) + + ADDED gluon/admin.pyc Index: gluon/admin.pyc ================================================================== --- gluon/admin.pyc +++ gluon/admin.pyc cannot compute difference between binary files ADDED gluon/cache.py Index: gluon/cache.py ================================================================== --- gluon/cache.py +++ gluon/cache.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +This file is part of the web2py Web Framework +Copyrighted by Massimo Di Pierro +License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) + +Basic caching classes and methods +================================= + +- Cache - The generic caching object interfacing with the others +- CacheInRam - providing caching in ram +- CacheInDisk - provides caches on disk + +Memcache is also available via a different module (see gluon.contrib.memcache) + +When web2py is running on Google App Engine, +caching will be provided by the GAE memcache +(see gluon.contrib.gae_memcache) +""" + +import time +import portalocker +import shelve +import thread +import os +import logging +import re + +logger = logging.getLogger("web2py.cache") + +__all__ = ['Cache'] + + +DEFAULT_TIME_EXPIRE = 300 + + +class CacheAbstract(object): + """ + Abstract class for cache implementations. + Main function is now to provide referenced api documentation. + + Use CacheInRam or CacheOnDisk instead which are derived from this class. + """ + + cache_stats_name = 'web2py_cache_statistics' + + def __init__(self, request=None): + """ + Paremeters + ---------- + request: + the global request object + """ + raise NotImplementedError + + def __call__(self, key, f, + time_expire = DEFAULT_TIME_EXPIRE): + """ + Tries retrieve the value corresponding to `key` from the cache of the + object exists and if it did not expire, else it called the function `f` + and stores the output in the cache corresponding to `key`. In the case + the output of the function is returned. + + :param key: the key of the object to be store or retrieved + :param f: the function, whose output is to be cached + :param time_expire: expiration of the cache in microseconds + + - `time_expire` is used to compare the current time with the time when + the requested object was last saved in cache. It does not affect + future requests. + - Setting `time_expire` to 0 or negative value forces the cache to + refresh. + + If the function `f` is `None` the cache is cleared. + """ + raise NotImplementedError + + def clear(self, regex=None): + """ + Clears the cache of all keys that match the provided regular expression. + If no regular expression is provided, it clears all entries in cache. + + Parameters + ---------- + regex: + if provided, only keys matching the regex will be cleared. + Otherwise all keys are cleared. + """ + + raise NotImplementedError + + def increment(self, key, value=1): + """ + Increments the cached value for the given key by the amount in value + + Parameters + ---------- + key: + key for the cached object to be incremeneted + value: + amount of the increment (defaults to 1, can be negative) + """ + raise NotImplementedError + + def _clear(self, storage, regex): + """ + Auxiliary function called by `clear` to search and clear cache entries + """ + r = re.compile(regex) + for (key, value) in storage.items(): + if r.match(str(key)): + del storage[key] + +class CacheInRam(CacheAbstract): + """ + Ram based caching + + This is implemented as global (per process, shared by all threads) + dictionary. + A mutex-lock mechanism avoid conflicts. + """ + + locker = thread.allocate_lock() + meta_storage = {} + + def __init__(self, request=None): + self.locker.acquire() + self.request = request + if request: + app = request.application + else: + app = '' + if not app in self.meta_storage: + self.storage = self.meta_storage[app] = {CacheAbstract.cache_stats_name: { + 'hit_total': 0, + 'misses': 0, + }} + else: + self.storage = self.meta_storage[app] + self.locker.release() + + def clear(self, regex=None): + self.locker.acquire() + storage = self.storage + if regex is None: + storage.clear() + else: + self._clear(storage, regex) + + if not CacheAbstract.cache_stats_name in storage.keys(): + storage[CacheAbstract.cache_stats_name] = { + 'hit_total': 0, + 'misses': 0, + } + + self.locker.release() + + def __call__(self, key, f, + time_expire = DEFAULT_TIME_EXPIRE): + """ + Attention! cache.ram does not copy the cached object. It just stores a reference to it. + Turns out the deepcopying the object has some problems: + 1) would break backward compatibility + 2) would be limiting because people may want to cache live objects + 3) would work unless we deepcopy no storage and retrival which would make things slow. + Anyway. You can deepcopy explicitly in the function generating the value to be cached. + """ + + dt = time_expire + + self.locker.acquire() + item = self.storage.get(key, None) + if item and f is None: + del self.storage[key] + self.storage[CacheAbstract.cache_stats_name]['hit_total'] += 1 + self.locker.release() + + if f is None: + return None + if item and (dt is None or item[0] > time.time() - dt): + return item[1] + value = f() + + self.locker.acquire() + self.storage[key] = (time.time(), value) + self.storage[CacheAbstract.cache_stats_name]['misses'] += 1 + self.locker.release() + return value + + def increment(self, key, value=1): + self.locker.acquire() + try: + if key in self.storage: + value = self.storage[key][1] + value + self.storage[key] = (time.time(), value) + except BaseException, e: + self.locker.release() + raise e + self.locker.release() + return value + + +class CacheOnDisk(CacheAbstract): + """ + Disk based cache + + This is implemented as a shelve object and it is shared by multiple web2py + processes (and threads) as long as they share the same filesystem. + The file is locked wen accessed. + + Disk cache provides persistance when web2py is started/stopped but it slower + than `CacheInRam` + + Values stored in disk cache must be pickable. + """ + + speedup_checks = set() + + def __init__(self, request, folder=None): + self.request = request + + # Lets test if the cache folder exists, if not + # we are going to create it + folder = folder or os.path.join(request.folder, 'cache') + + if not os.path.exists(folder): + os.mkdir(folder) + + ### we need this because of a possible bug in shelve that may + ### or may not lock + self.locker_name = os.path.join(folder,'cache.lock') + self.shelve_name = os.path.join(folder,'cache.shelve') + + locker, locker_locked = None, False + speedup_key = (folder,CacheAbstract.cache_stats_name) + if not speedup_key in self.speedup_checks or \ + not os.path.exists(self.shelve_name): + try: + locker = open(self.locker_name, 'a') + portalocker.lock(locker, portalocker.LOCK_EX) + locker_locked = True + storage = shelve.open(self.shelve_name) + try: + if not storage.has_key(CacheAbstract.cache_stats_name): + storage[CacheAbstract.cache_stats_name] = { + 'hit_total': 0, + 'misses': 0, + } + storage.sync() + finally: + storage.close() + self.speedup_checks.add(speedup_key) + except ImportError: + pass # no module _bsddb, ignoring exception now so it makes a ticket only if used + except: + logger.error('corrupted file %s, will try delete it!' \ + % self.shelve_name) + try: + os.unlink(self.shelve_name) + except IOError: + logger.warn('unable to delete file %s' % self.shelve_name) + if locker_locked: + portalocker.unlock(locker) + if locker: + locker.close() + + def clear(self, regex=None): + locker = open(self.locker_name,'a') + portalocker.lock(locker, portalocker.LOCK_EX) + storage = shelve.open(self.shelve_name) + try: + if regex is None: + storage.clear() + else: + self._clear(storage, regex) + if not CacheAbstract.cache_stats_name in storage.keys(): + storage[CacheAbstract.cache_stats_name] = { + 'hit_total': 0, + 'misses': 0, + } + storage.sync() + finally: + storage.close() + portalocker.unlock(locker) + locker.close() + + def __call__(self, key, f, + time_expire = DEFAULT_TIME_EXPIRE): + dt = time_expire + + locker = open(self.locker_name,'a') + portalocker.lock(locker, portalocker.LOCK_EX) + storage = shelve.open(self.shelve_name) + + item = storage.get(key, None) + if item and f is None: + del storage[key] + + storage[CacheAbstract.cache_stats_name] = { + 'hit_total': storage[CacheAbstract.cache_stats_name]['hit_total'] + 1, + 'misses': storage[CacheAbstract.cache_stats_name]['misses'] + } + + storage.sync() + + portalocker.unlock(locker) + locker.close() + + if f is None: + return None + if item and (dt is None or item[0] > time.time() - dt): + return item[1] + value = f() + + locker = open(self.locker_name,'a') + portalocker.lock(locker, portalocker.LOCK_EX) + storage[key] = (time.time(), value) + + storage[CacheAbstract.cache_stats_name] = { + 'hit_total': storage[CacheAbstract.cache_stats_name]['hit_total'], + 'misses': storage[CacheAbstract.cache_stats_name]['misses'] + 1 + } + + storage.sync() + + storage.close() + portalocker.unlock(locker) + locker.close() + + return value + + def increment(self, key, value=1): + locker = open(self.locker_name,'a') + portalocker.lock(locker, portalocker.LOCK_EX) + storage = shelve.open(self.shelve_name) + try: + if key in storage: + value = storage[key][1] + value + storage[key] = (time.time(), value) + storage.sync() + finally: + storage.close() + portalocker.unlock(locker) + locker.close() + return value + + +class Cache(object): + """ + Sets up generic caching, creating an instance of both CacheInRam and + CacheOnDisk. + In case of GAE will make use of gluon.contrib.gae_memcache. + + - self.ram is an instance of CacheInRam + - self.disk is an instance of CacheOnDisk + """ + + def __init__(self, request): + """ + Parameters + ---------- + request: + the global request object + """ + # GAE will have a special caching + import settings + if settings.global_settings.web2py_runtime_gae: + from contrib.gae_memcache import MemcacheClient + self.ram=self.disk=MemcacheClient(request) + else: + # Otherwise use ram (and try also disk) + self.ram = CacheInRam(request) + try: + self.disk = CacheOnDisk(request) + except IOError: + logger.warning('no cache.disk (IOError)') + except AttributeError: + # normally not expected anymore, as GAE has already + # been accounted for + logger.warning('no cache.disk (AttributeError)') + + def __call__(self, + key = None, + time_expire = DEFAULT_TIME_EXPIRE, + cache_model = None): + """ + Decorator function that can be used to cache any function/method. + + Example:: + + @cache('key', 5000, cache.ram) + def f(): + return time.ctime() + + When the function f is called, web2py tries to retrieve + the value corresponding to `key` from the cache of the + object exists and if it did not expire, else it calles the function `f` + and stores the output in the cache corresponding to `key`. In the case + the output of the function is returned. + + :param key: the key of the object to be store or retrieved + :param time_expire: expiration of the cache in microseconds + :param cache_model: `cache.ram`, `cache.disk`, or other + (like `cache.memcache` if defined). It defaults to `cache.ram`. + + Notes + ----- + `time_expire` is used to compare the curret time with the time when the + requested object was last saved in cache. It does not affect future + requests. + Setting `time_expire` to 0 or negative value forces the cache to + refresh. + + If the function `f` is an action, we suggest using + `request.env.path_info` as key. + """ + if not cache_model: + cache_model = self.ram + + def tmp(func): + def action(): + return cache_model(key, func, time_expire) + action.__name___ = func.__name__ + action.__doc__ = func.__doc__ + return action + + return tmp + + + ADDED gluon/cache.pyc Index: gluon/cache.pyc ================================================================== --- gluon/cache.pyc +++ gluon/cache.pyc cannot compute difference between binary files Index: gluon/cfs.py ================================================================== --- gluon/cfs.py +++ gluon/cfs.py @@ -46,6 +46,8 @@ data = filter() cfs_lock.acquire() cfs[key] = (t, data) cfs_lock.release() return data + + ADDED gluon/cfs.pyc Index: gluon/cfs.pyc ================================================================== --- gluon/cfs.pyc +++ gluon/cfs.pyc cannot compute difference between binary files Index: gluon/compileapp.py ================================================================== --- gluon/compileapp.py +++ gluon/compileapp.py @@ -88,37 +88,37 @@ _TEST() """ 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..... """ #__builtins__ def __getitem__(self, key): try: return getattr(__builtin__, key) except AttributeError: - raise KeyError, key + raise KeyError, key def __setitem__(self, key, value): setattr(self, key, value) class LoadFactory(object): """ Attention: this helper is new and experimental """ def __init__(self,environment): self.environment = environment - def __call__(self, c=None, f='index', args=[], vars={}, + def __call__(self, c=None, f='index', args=None, vars=None, extension=None, target=None,ajax=False,ajax_trap=False, url=None,user_signature=False, content='loading...',**attr): + if args is None: args = [] + vars = Storage(vars or {}) import globals target = target or 'c'+str(random.random())[2:] attr['_id']=target request = self.environment['request'] - if not isinstance(vars,Storage): - vars = Storage(vars) if '.' in f: f, extension = f.split('.',1) if url or ajax: url = url or html.URL(request.application, c, f, r=request, args=args, vars=vars, extension=extension, @@ -134,11 +134,11 @@ other_request = Storage() for key, value in request.items(): other_request[key] = value other_request['env'] = Storage() for key, value in request.env.items(): - other_request.env['key'] = value + other_request.env['key'] = value other_request.controller = c other_request.function = f other_request.extension = extension or request.extension other_request.args = List(args) other_request.vars = vars @@ -149,11 +149,11 @@ '/'.join([request.application,c,f] + \ map(str, other_request.args)) other_request.env.query_string = \ vars and html.URL(vars=vars).split('?')[1] or '' other_request.env.http_web2py_component_location = \ - request.env.path_info + request.env.path_info other_request.cid = target other_request.env.http_web2py_component_element = target other_response.view = '%s/%s.%s' % (c,f, other_request.extension) other_environment = copy.copy(self.environment) other_response._view_environment = other_environment @@ -264,11 +264,11 @@ global __builtins__ if is_jython: # jython hack __builtins__ = mybuiltin() else: - __builtins__['__import__'] = __builtin__.__import__ + __builtins__['__import__'] = __builtin__.__import__ ### WHY? environment['__builtins__'] = __builtins__ environment['HTTP'] = HTTP environment['redirect'] = redirect environment['request'] = request environment['response'] = response @@ -312,11 +312,11 @@ """ Compiles all the views in the application specified by `folder` """ path = os.path.join(folder, 'views') - for file in listdir(path, '^[\w/]+\.\w+$'): + for file in listdir(path, '^[\w/\-]+(\.\w+)+$'): data = parse_template(file, path) filename = ('views/%s.py' % file).replace('/', '_').replace('\\', '_') filename = os.path.join(folder, 'compiled', filename) write_file(filename, data) save_pyc(filename) @@ -566,6 +566,8 @@ if __name__ == '__main__': import doctest doctest.testmod() + + ADDED gluon/compileapp.pyc Index: gluon/compileapp.pyc ================================================================== --- gluon/compileapp.pyc +++ gluon/compileapp.pyc cannot compute difference between binary files Index: gluon/contenttype.py ================================================================== --- gluon/contenttype.py +++ gluon/contenttype.py @@ -242,10 +242,11 @@ '.jpg': 'image/jpeg', '.jpr': 'application/x-jbuilder-project', '.jpx': 'image/jp2', '.js': 'application/javascript', '.json': 'application/json', + '.jsonp': 'application/jsonp', '.k25': 'image/x-kodak-k25', '.kar': 'audio/midi', '.karbon': 'application/x-karbon', '.kdc': 'image/x-kodak-kdc', '.kdelnk': 'application/x-desktop', @@ -713,6 +714,8 @@ if j>=0: default = CONTENT_TYPE.get(filename[j:].lower(),default) if default.startswith('text/'): default += '; charset=utf-8' return default + + ADDED gluon/contenttype.pyc Index: gluon/contenttype.pyc ================================================================== --- gluon/contenttype.pyc +++ gluon/contenttype.pyc cannot compute difference between binary files Index: gluon/contrib/AuthorizeNet.py ================================================================== --- gluon/contrib/AuthorizeNet.py +++ gluon/contrib/AuthorizeNet.py @@ -255,6 +255,7 @@ print 'declined',payment.isDeclined() print 'error',payment.isError() if __name__=='__main__': test() + Index: gluon/contrib/__init__.py ================================================================== --- gluon/contrib/__init__.py +++ gluon/contrib/__init__.py @@ -1,2 +1,3 @@ + Index: gluon/contrib/comet_messaging.py ================================================================== --- gluon/contrib/comet_messaging.py +++ gluon/contrib/comet_messaging.py @@ -4,11 +4,11 @@ Copyrighted by Massimo Di Pierro License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) Attention: Requires Chrome or Safari. For IE of Firefox you need https://github.com/gimite/web-socket-js -1) install tornado +1) install tornado (requires Tornado 2.1) easy_install tornado 2) start this app: @@ -186,6 +186,7 @@ (r'/realtime/(.*)', DistributeHandler)] application = tornado.web.Application(urls, auto_reload=True) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(int(options.port), address=options.address) tornado.ioloop.IOLoop.instance().start() + Index: gluon/contrib/feedparser.py ================================================================== --- gluon/contrib/feedparser.py +++ gluon/contrib/feedparser.py @@ -1,23 +1,19 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- - """Universal feed parser Handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds Visit http://feedparser.org/ for the latest version Visit http://feedparser.org/docs/ for the latest documentation -Required: Python 2.1 or later -Recommended: Python 2.3 or later +Required: Python 2.4 or later Recommended: CJKCodecs and iconv_codec -""" # + "$Revision: 1.92 $"[11:15] + "-cvs" +""" -__version__ = '4.1' -__license__ = \ - """Copyright (c) 2002-2006, Mark Pilgrim, All rights reserved. +__version__ = "5.0.1" +__license__ = """Copyright (c) 2002-2008, Mark Pilgrim, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, @@ -35,691 +31,509 @@ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.""" -__author__ = 'Mark Pilgrim ' -__contributors__ = ['Jason Diamond ', - 'John Beimler ', - 'Fazal Majid ' - , 'Aaron Swartz ', - 'Kevin Marks '] -_debug = 0 +__author__ = "Mark Pilgrim " +__contributors__ = ["Jason Diamond ", + "John Beimler ", + "Fazal Majid ", + "Aaron Swartz ", + "Kevin Marks ", + "Sam Ruby ", + "Ade Oshineye ", + "Martin Pool ", + "Kurt McKee "] # HTTP "User-Agent" header to send to servers when downloading feeds. # If you are embedding feedparser in a larger application, you should # change this to your application name and URL. - -USER_AGENT = 'UniversalFeedParser/%s +http://feedparser.org/'\ - % __version__ +USER_AGENT = "UniversalFeedParser/%s +http://feedparser.org/" % __version__ # HTTP "Accept" header to send to servers when downloading feeds. If you don't # want to send an Accept header, set this to None. - -ACCEPT_HEADER = \ - 'application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1' +ACCEPT_HEADER = "application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1" # List of preferred XML parsers, by SAX driver name. These will be tried first, # but if they're not installed, Python will keep searching through its own list # of pre-installed parsers until it finds one that supports everything we need. - -PREFERRED_XML_PARSERS = ['drv_libxml2'] +PREFERRED_XML_PARSERS = ["drv_libxml2"] # If you want feedparser to automatically run HTML markup through HTML Tidy, set # this to 1. Requires mxTidy # or utidylib . - TIDY_MARKUP = 0 # List of Python interfaces for HTML Tidy, in order of preference. Only useful # if TIDY_MARKUP = 1 +PREFERRED_TIDY_INTERFACES = ["uTidy", "mxTidy"] -PREFERRED_TIDY_INTERFACES = ['uTidy', 'mxTidy'] +# If you want feedparser to automatically resolve all relative URIs, set this +# to 1. +RESOLVE_RELATIVE_URIS = 1 + +# If you want feedparser to automatically sanitize all potentially unsafe +# HTML content, set this to 1. +SANITIZE_HTML = 1 + +# ---------- Python 3 modules (make it work if possible) ---------- +try: + import rfc822 +except ImportError: + from email import _parseaddr as rfc822 + +try: + # Python 3.1 introduces bytes.maketrans and simultaneously + # deprecates string.maketrans; use bytes.maketrans if possible + _maketrans = bytes.maketrans +except (NameError, AttributeError): + import string + _maketrans = string.maketrans + +# base64 support for Atom feeds that contain embedded binary data +try: + import base64, binascii +except ImportError: + base64 = binascii = None +else: + # Python 3.1 deprecates decodestring in favor of decodebytes + _base64decode = getattr(base64, 'decodebytes', base64.decodestring) + +def _s2bytes(s): + # Convert a UTF-8 str to bytes if the interpreter is Python 3 + try: + return bytes(s, 'utf8') + except (NameError, TypeError): + # In Python 2.5 and below, bytes doesn't exist (NameError) + # In Python 2.6 and above, bytes and str are the same (TypeError) + return s + +def _l2bytes(l): + # Convert a list of ints to bytes if the interpreter is Python 3 + try: + if bytes is not str: + # In Python 2.6 and above, this call won't raise an exception + # but it will return bytes([65]) as '[65]' instead of 'A' + return bytes(l) + raise NameError + except NameError: + return ''.join(map(chr, l)) + +# If you want feedparser to allow all URL schemes, set this to () +# List culled from Python's urlparse documentation at: +# http://docs.python.org/library/urlparse.html +# as well as from "URI scheme" at Wikipedia: +# https://secure.wikimedia.org/wikipedia/en/wiki/URI_scheme +# Many more will likely need to be added! +ACCEPTABLE_URI_SCHEMES = ( + 'file', 'ftp', 'gopher', 'h323', 'hdl', 'http', 'https', 'imap', 'mailto', + 'mms', 'news', 'nntp', 'prospero', 'rsync', 'rtsp', 'rtspu', 'sftp', + 'shttp', 'sip', 'sips', 'snews', 'svn', 'svn+ssh', 'telnet', 'wais', + # Additional common-but-unofficial schemes + 'aim', 'callto', 'cvs', 'facetime', 'feed', 'git', 'gtalk', 'irc', 'ircs', + 'irc6', 'itms', 'mms', 'msnim', 'skype', 'ssh', 'smb', 'svn', 'ymsg', +) +#ACCEPTABLE_URI_SCHEMES = () # ---------- required modules (should come with any Python distribution) ---------- - -import sgmllib +import cgi +import copy +import datetime import re +import struct import sys -import copy -import urlparse import time -import rfc822 import types -import cgi import urllib import urllib2 +import urlparse + +from htmlentitydefs import name2codepoint, codepoint2name, entitydefs + try: - from cStringIO import StringIO as _StringIO -except: - from StringIO import StringIO as _StringIO + from io import BytesIO as _StringIO +except ImportError: + try: + from cStringIO import StringIO as _StringIO + except ImportError: + from StringIO import StringIO as _StringIO # ---------- optional modules (feedparser will work without these, but with reduced functionality) ---------- # gzip is included with most Python distributions, but may not be available if you compiled your own - try: import gzip -except: +except ImportError: gzip = None try: import zlib -except: +except ImportError: zlib = None # If a real XML parser is available, feedparser will attempt to use it. feedparser has # been tested with the built-in SAX parser, PyXML, and libxml2. On platforms where the # Python distribution does not come with an XML parser (such as Mac OS X 10.2 and some # versions of FreeBSD), feedparser will quietly fall back on regex-based parsing. - try: import xml.sax - xml.sax.make_parser(PREFERRED_XML_PARSERS) # test for valid parsers from xml.sax.saxutils import escape as _xmlescape - _XML_AVAILABLE = 1 -except: +except ImportError: _XML_AVAILABLE = 0 - - - def _xmlescape(data): + def _xmlescape(data,entities={}): data = data.replace('&', '&') data = data.replace('>', '>') data = data.replace('<', '<') + for char, entity in entities: + data = data.replace(char, entity) return data +else: + try: + xml.sax.make_parser(PREFERRED_XML_PARSERS) # test for valid parsers + except xml.sax.SAXReaderNotAvailable: + _XML_AVAILABLE = 0 + else: + _XML_AVAILABLE = 1 - -# base64 support for Atom feeds that contain embedded binary data - +# sgmllib is not available by default in Python 3; if the end user doesn't have +# it available then we'll lose illformed XML parsing, content santizing, and +# microformat support (at least while feedparser depends on BeautifulSoup). try: - import base64 - import binascii -except: - base64 = binascii = None + import sgmllib +except ImportError: + # This is probably Python 3, which doesn't include sgmllib anymore + _SGML_AVAILABLE = 0 + + # Mock sgmllib enough to allow subclassing later on + class sgmllib(object): + class SGMLParser(object): + def goahead(self, i): + pass + def parse_starttag(self, i): + pass +else: + _SGML_AVAILABLE = 1 + + # sgmllib defines a number of module-level regular expressions that are + # insufficient for the XML parsing feedparser needs. Rather than modify + # the variables directly in sgmllib, they're defined here using the same + # names, and the compiled code objects of several sgmllib.SGMLParser + # methods are copied into _BaseHTMLProcessor so that they execute in + # feedparser's scope instead of sgmllib's scope. + charref = re.compile('&#(\d+|[xX][0-9a-fA-F]+);') + tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') + + # Unfortunately, these must be copied over to prevent NameError exceptions + attrfind = sgmllib.attrfind + entityref = sgmllib.entityref + incomplete = sgmllib.incomplete + interesting = sgmllib.interesting + shorttag = sgmllib.shorttag + shorttagopen = sgmllib.shorttagopen + starttagopen = sgmllib.starttagopen + + class _EndBracketRegEx: + def __init__(self): + # Overriding the built-in sgmllib.endbracket regex allows the + # parser to find angle brackets embedded in element attributes. + self.endbracket = re.compile('''([^'"<>]|"[^"]*"(?=>|/|\s|\w+=)|'[^']*'(?=>|/|\s|\w+=))*(?=[<>])|.*?(?=[<>])''') + def search(self, target, index=0): + match = self.endbracket.match(target, index) + if match is not None: + # Returning a new object in the calling thread's context + # resolves a thread-safety. + return EndBracketMatch(match) + return None + class EndBracketMatch: + def __init__(self, match): + self.match = match + def start(self, n): + return self.match.end(n) + endbracket = _EndBracketRegEx() + # cjkcodecs and iconv_codec provide support for more character encodings. # Both are available from http://cjkpython.i18n.org/ - try: import cjkcodecs.aliases -except: +except ImportError: pass try: import iconv_codec -except: +except ImportError: pass # chardet library auto-detects character encodings # Download from http://chardet.feedparser.org/ - try: import chardet - if _debug: - import chardet.constants - chardet.constants._debug = 1 -except: +except ImportError: chardet = None + +# BeautifulSoup parser used for parsing microformats from embedded HTML content +# http://www.crummy.com/software/BeautifulSoup/ +# feedparser is tested with BeautifulSoup 3.0.x, but it might work with the +# older 2.x series. If it doesn't, and you can figure out why, I'll accept a +# patch and modify the compatibility statement accordingly. +try: + import BeautifulSoup +except ImportError: + BeautifulSoup = None # ---------- don't touch these ---------- - - -class ThingsNobodyCaresAboutButMe(Exception): - - pass - - -class CharacterEncodingOverride(ThingsNobodyCaresAboutButMe): - - pass - - -class CharacterEncodingUnknown(ThingsNobodyCaresAboutButMe): - - pass - - -class NonXMLContentType(ThingsNobodyCaresAboutButMe): - - pass - - -class UndeclaredNamespace(Exception): - - pass - - -sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') -sgmllib.special = re.compile('' % (tag, ''.join([' %s="%s"' - % t for t in attrs])), escape=0) + self.contentparams['type'] = u'application/xhtml+xml' + if self.incontent and self.contentparams.get('type') == u'application/xhtml+xml': + if tag.find(':') <> -1: + prefix, tag = tag.split(':', 1) + namespace = self.namespacesInUse.get(prefix, '') + if tag=='math' and namespace=='http://www.w3.org/1998/Math/MathML': + attrs.append(('xmlns',namespace)) + if tag=='svg' and namespace=='http://www.w3.org/2000/svg': + attrs.append(('xmlns',namespace)) + if tag == 'svg': + self.svgOK += 1 + return self.handle_data('<%s%s>' % (tag, self.strattrs(attrs)), escape=0) # match namespaces - - if tag.find(':') != -1: - (prefix, suffix) = tag.split(':', 1) + if tag.find(':') <> -1: + prefix, suffix = tag.split(':', 1) else: - (prefix, suffix) = ('', tag) + prefix, suffix = '', tag prefix = self.namespacemap.get(prefix, prefix) if prefix: prefix = prefix + '_' # special hack for better tracking of empty textinput/image elements in illformed feeds - - if not prefix and tag not in ('title', 'link', 'description', - 'name'): + if (not prefix) and tag not in ('title', 'link', 'description', 'name'): self.intextinput = 0 - if not prefix and tag not in ( - 'title', - 'link', - 'description', - 'url', - 'href', - 'width', - 'height', - ): + if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'): self.inimage = 0 # call special handler (if defined) or default handler - methodname = '_start_' + prefix + suffix try: method = getattr(self, methodname) return method(attrsD) except AttributeError: - return self.push(prefix + suffix, 1) + # Since there's no handler or something has gone wrong we explicitly add the element and its attributes + unknown_tag = prefix + suffix + if len(attrsD) == 0: + # No attributes so merge it into the encosing dictionary + return self.push(unknown_tag, 1) + else: + # Has attributes so create it in its own dictionary + context = self._getContext() + context[unknown_tag] = attrsD def unknown_endtag(self, tag): - if _debug: - sys.stderr.write('end %s\n' % tag) - # match namespaces - - if tag.find(':') != -1: - (prefix, suffix) = tag.split(':', 1) + if tag.find(':') <> -1: + prefix, suffix = tag.split(':', 1) else: - (prefix, suffix) = ('', tag) + prefix, suffix = '', tag prefix = self.namespacemap.get(prefix, prefix) if prefix: prefix = prefix + '_' + if suffix == 'svg' and self.svgOK: + self.svgOK -= 1 # call special handler (if defined) or default handler - methodname = '_end_' + prefix + suffix try: + if self.svgOK: + raise AttributeError() method = getattr(self, methodname) method() except AttributeError: self.pop(prefix + suffix) # track inline content - - if self.incontent and self.contentparams.has_key('type')\ - and not self.contentparams.get('type', 'xml' - ).endswith('xml'): - + if self.incontent and self.contentparams.has_key('type') and not self.contentparams.get('type', u'xml').endswith(u'xml'): # element declared itself as escaped markup, but it isn't really - - self.contentparams['type'] = 'application/xhtml+xml' - if self.incontent and self.contentparams.get('type')\ - == 'application/xhtml+xml': + if tag in ['xhtml:div', 'div']: + return # typepad does this 10/2007 + self.contentparams['type'] = u'application/xhtml+xml' + if self.incontent and self.contentparams.get('type') == u'application/xhtml+xml': tag = tag.split(':')[-1] self.handle_data('' % tag, escape=0) # track xml:base and xml:lang going out of scope - if self.basestack: self.basestack.pop() if self.basestack and self.basestack[-1]: self.baseuri = self.basestack[-1] if self.langstack: self.langstack.pop() - if self.langstack: # and (self.langstack[-1] is not None): + if self.langstack: # and (self.langstack[-1] is not None): self.lang = self.langstack[-1] def handle_charref(self, ref): - # called for each character reference, e.g. for ' ', ref will be '160' - if not self.elementstack: return ref = ref.lower() - if ref in ( - '34', - '38', - '39', - '60', - '62', - 'x22', - 'x26', - 'x27', - 'x3c', - 'x3e', - ): + if ref in ('34', '38', '39', '60', '62', 'x22', 'x26', 'x27', 'x3c', 'x3e'): text = '&#%s;' % ref else: if ref[0] == 'x': c = int(ref[1:], 16) else: @@ -916,258 +708,315 @@ c = int(ref) text = unichr(c).encode('utf-8') self.elementstack[-1][2].append(text) def handle_entityref(self, ref): - # called for each entity reference, e.g. for '©', ref will be 'copy' - if not self.elementstack: return - if _debug: - sys.stderr.write('entering handle_entityref with %s\n' - % ref) if ref in ('lt', 'gt', 'quot', 'amp', 'apos'): text = '&%s;' % ref + elif ref in self.entities.keys(): + text = self.entities[ref] + if text.startswith('&#') and text.endswith(';'): + return self.handle_entityref(text) else: - - # entity resolution graciously donated by Aaron Swartz - - def name2cp(k): - import htmlentitydefs - if hasattr(htmlentitydefs, 'name2codepoint'): # requires Python 2.3 - return htmlentitydefs.name2codepoint[k] - k = htmlentitydefs.entitydefs[k] - if k.startswith('&#') and k.endswith(';'): - return int(k[2:-1]) # not in latin-1 - return ord(k) - try: - name2cp(ref) + name2codepoint[ref] except KeyError: text = '&%s;' % ref else: - text = unichr(name2cp(ref)).encode('utf-8') + text = unichr(name2codepoint[ref]).encode('utf-8') self.elementstack[-1][2].append(text) def handle_data(self, text, escape=1): - # called for each block of plain text, i.e. outside of any tag and # not containing any character or entity references - if not self.elementstack: return - if escape and self.contentparams.get('type')\ - == 'application/xhtml+xml': + if escape and self.contentparams.get('type') == u'application/xhtml+xml': text = _xmlescape(text) self.elementstack[-1][2].append(text) def handle_comment(self, text): - # called for each comment, e.g. - pass def handle_pi(self, text): - # called for each processing instruction, e.g. - pass def handle_decl(self, text): pass def parse_declaration(self, i): - # override internal declaration handler to handle CDATA blocks - - if _debug: - sys.stderr.write('entering parse_declaration\n') - if self.rawdata[i:i + 9] == '', i) if k == -1: + # CDATA block began but didn't finish k = len(self.rawdata) - self.handle_data(_xmlescape(self.rawdata[i + 9:k]), 0) - return k + 3 + return k + self.handle_data(_xmlescape(self.rawdata[i+9:k]), 0) + return k+3 else: k = self.rawdata.find('>', i) - return k + 1 + if k >= 0: + return k+1 + else: + # We have an incomplete CDATA block. + return k def mapContentType(self, contentType): contentType = contentType.lower() - if contentType == 'text': - contentType = 'text/plain' + if contentType == 'text' or contentType == 'plain': + contentType = u'text/plain' elif contentType == 'html': - contentType = 'text/html' + contentType = u'text/html' elif contentType == 'xhtml': - contentType = 'application/xhtml+xml' + contentType = u'application/xhtml+xml' return contentType def trackNamespace(self, prefix, uri): loweruri = uri.lower() - if (prefix, loweruri) == (None, - 'http://my.netscape.com/rdf/simple/0.9/' - ) and not self.version: - self.version = 'rss090' + if (prefix, loweruri) == (None, 'http://my.netscape.com/rdf/simple/0.9/') and not self.version: + self.version = u'rss090' if loweruri == 'http://purl.org/rss/1.0/' and not self.version: - self.version = 'rss10' - if loweruri == 'http://www.w3.org/2005/atom'\ - and not self.version: - self.version = 'atom10' - if loweruri.find('backend.userland.com/rss') != -1: - + self.version = u'rss10' + if loweruri == 'http://www.w3.org/2005/atom' and not self.version: + self.version = u'atom10' + if loweruri.find(u'backend.userland.com/rss') <> -1: # match any backend.userland.com namespace - - uri = 'http://backend.userland.com/rss' + uri = u'http://backend.userland.com/rss' loweruri = uri if self._matchnamespaces.has_key(loweruri): self.namespacemap[prefix] = self._matchnamespaces[loweruri] self.namespacesInUse[self._matchnamespaces[loweruri]] = uri else: self.namespacesInUse[prefix or ''] = uri def resolveURI(self, uri): - return _urljoin(self.baseuri or '', uri) + return _urljoin(self.baseuri or u'', uri) def decodeEntities(self, element, data): return data + def strattrs(self, attrs): + return ''.join([' %s="%s"' % (t[0],_xmlescape(t[1],{'"':'"'})) for t in attrs]) + def push(self, element, expectingText): self.elementstack.append([element, expectingText, []]) def pop(self, element, stripWhitespace=1): if not self.elementstack: return if self.elementstack[-1][0] != element: return - (element, expectingText, pieces) = self.elementstack.pop() - output = ''.join(pieces) + element, expectingText, pieces = self.elementstack.pop() + + if self.version == u'atom10' and self.contentparams.get('type', u'text') == u'application/xhtml+xml': + # remove enclosing child element, but only if it is a
    and + # only if all the remaining content is nested underneath it. + # This means that the divs would be retained in the following: + #
    foo
    bar
    + while pieces and len(pieces)>1 and not pieces[-1].strip(): + del pieces[-1] + while pieces and len(pieces)>1 and not pieces[0].strip(): + del pieces[0] + if pieces and (pieces[0] == '
    ' or pieces[0].startswith('
    ': + depth = 0 + for piece in pieces[:-1]: + if piece.startswith(''): + depth += 1 + else: + pieces = pieces[1:-1] + + # Ensure each piece is a str for Python 3 + for (i, v) in enumerate(pieces): + if not isinstance(v, unicode): + pieces[i] = v.decode('utf-8') + + output = u''.join(pieces) if stripWhitespace: output = output.strip() if not expectingText: return output # decode base64 content - if base64 and self.contentparams.get('base64', 0): try: - output = base64.decodestring(output) + output = _base64decode(output) except binascii.Error: pass except binascii.Incomplete: pass + except TypeError: + # In Python 3, base64 takes and outputs bytes, not str + # This may not be the most correct way to accomplish this + output = _base64decode(output.encode('utf-8')).decode('utf-8') # resolve relative URIs - - if element in self.can_be_relative_uri and output: + if (element in self.can_be_relative_uri) and output: output = self.resolveURI(output) # decode entities within embedded markup - if not self.contentparams.get('base64', 0): output = self.decodeEntities(element, output) + # some feed formats require consumers to guess + # whether the content is html or plain text + if not self.version.startswith(u'atom') and self.contentparams.get('type') == u'text/plain': + if self.lookslikehtml(output): + self.contentparams['type'] = u'text/html' + # remove temporary cruft from contentparams - try: del self.contentparams['mode'] except KeyError: pass try: del self.contentparams['base64'] except KeyError: pass + is_htmlish = self.mapContentType(self.contentparams.get('type', u'text/html')) in self.html_types # resolve relative URIs within embedded markup - - if self.mapContentType(self.contentparams.get('type', - 'text/html')) in self.html_types: + if is_htmlish and RESOLVE_RELATIVE_URIS: if element in self.can_contain_relative_uris: - output = _resolveRelativeURIs(output, self.baseuri, - self.encoding) + output = _resolveRelativeURIs(output, self.baseuri, self.encoding, self.contentparams.get('type', u'text/html')) + + # parse microformats + # (must do this before sanitizing because some microformats + # rely on elements that we sanitize) + if is_htmlish and element in ['content', 'description', 'summary']: + mfresults = _parseMicroformats(output, self.baseuri, self.encoding) + if mfresults: + for tag in mfresults.get('tags', []): + self._addTag(tag['term'], tag['scheme'], tag['label']) + for enclosure in mfresults.get('enclosures', []): + self._start_enclosure(enclosure) + for xfn in mfresults.get('xfn', []): + self._addXFN(xfn['relationships'], xfn['href'], xfn['name']) + vcard = mfresults.get('vcard') + if vcard: + self._getContext()['vcard'] = vcard # sanitize embedded markup - - if self.mapContentType(self.contentparams.get('type', - 'text/html')) in self.html_types: + if is_htmlish and SANITIZE_HTML: if element in self.can_contain_dangerous_markup: - output = _sanitizeHTML(output, self.encoding) + output = _sanitizeHTML(output, self.encoding, self.contentparams.get('type', u'text/html')) - if self.encoding and type(output) != type(u''): + if self.encoding and not isinstance(output, unicode): + output = output.decode(self.encoding, 'ignore') + + # address common error where people take data that is already + # utf-8, presume that it is iso-8859-1, and re-encode it. + if self.encoding in (u'utf-8', u'utf-8_INVALID_PYTHON_3') and isinstance(output, unicode): try: - output = unicode(output, self.encoding) - except: + output = output.encode('iso-8859-1').decode('utf-8') + except (UnicodeEncodeError, UnicodeDecodeError): pass + # map win-1252 extensions to the proper code points + if isinstance(output, unicode): + output = u''.join([c in _cp1252.keys() and _cp1252[c] or c for c in output]) + # categories/tags/keywords/whatever are handled in _end_category - if element == 'category': return output + + if element == 'title' and self.hasTitle: + return output # store output in appropriate place(s) - if self.inentry and not self.insource: if element == 'content': self.entries[-1].setdefault(element, []) contentparams = copy.deepcopy(self.contentparams) contentparams['value'] = output self.entries[-1][element].append(contentparams) elif element == 'link': - self.entries[-1][element] = output - if output: - self.entries[-1]['links'][-1]['href'] = output + if not self.inimage: + # query variables in urls in link elements are improperly + # converted from `?a=1&b=2` to `?a=1&b;=2` as if they're + # unhandled character references. fix this special case. + output = re.sub("&([A-Za-z0-9_]+);", "&\g<1>", output) + self.entries[-1][element] = output + if output: + self.entries[-1]['links'][-1]['href'] = output else: if element == 'description': element = 'summary' self.entries[-1][element] = output if self.incontent: contentparams = copy.deepcopy(self.contentparams) contentparams['value'] = output - self.entries[-1][element + '_detail'] = \ - contentparams - elif (self.infeed or self.insource) and not self.intextinput\ - and not self.inimage: + self.entries[-1][element + '_detail'] = contentparams + elif (self.infeed or self.insource):# and (not self.intextinput) and (not self.inimage): context = self._getContext() if element == 'description': element = 'subtitle' context[element] = output if element == 'link': + # fix query variables; see above for the explanation + output = re.sub("&([A-Za-z0-9_]+);", "&\g<1>", output) + context[element] = output context['links'][-1]['href'] = output elif self.incontent: contentparams = copy.deepcopy(self.contentparams) contentparams['value'] = output context[element + '_detail'] = contentparams return output - def pushContent( - self, - tag, - attrsD, - defaultContentType, - expectingText, - ): + def pushContent(self, tag, attrsD, defaultContentType, expectingText): self.incontent += 1 - self.contentparams = FeedParserDict({'type' - : self.mapContentType(attrsD.get('type', - defaultContentType)), 'language': self.lang, 'base' - : self.baseuri}) - self.contentparams['base64'] = self._isBase64(attrsD, - self.contentparams) + if self.lang: + self.lang=self.lang.replace('_','-') + self.contentparams = FeedParserDict({ + 'type': self.mapContentType(attrsD.get('type', defaultContentType)), + 'language': self.lang, + 'base': self.baseuri}) + self.contentparams['base64'] = self._isBase64(attrsD, self.contentparams) self.push(tag, expectingText) def popContent(self, tag): value = self.pop(tag) self.incontent -= 1 self.contentparams.clear() return value + + # a number of elements in a number of RSS variants are nominally plain + # text, but this is routinely ignored. This is an attempt to detect + # the most common cases. As false positives often result in silent + # data loss, this function errs on the conservative side. + @staticmethod + def lookslikehtml(s): + # must have a close tag or a entity reference to qualify + if not (re.search(r'',s) or re.search("&#?\w+;",s)): + return + + # all tags must be in a restricted subset of valid HTML tags + if filter(lambda t: t.lower() not in _HTMLSanitizer.acceptable_elements, + re.findall(r' -1: prefix = name[:colonpos] - suffix = name[colonpos + 1:] + suffix = name[colonpos+1:] prefix = self.namespacemap.get(prefix, prefix) name = prefix + ':' + suffix return name def _getAttribute(self, attrsD, name): @@ -1174,21 +1023,20 @@ return attrsD.get(self._mapToStandardPrefix(name)) def _isBase64(self, attrsD, contentparams): if attrsD.get('mode', '') == 'base64': return 1 - if self.contentparams['type'].startswith('text/'): + if self.contentparams['type'].startswith(u'text/'): return 0 - if self.contentparams['type'].endswith('+xml'): + if self.contentparams['type'].endswith(u'+xml'): return 0 - if self.contentparams['type'].endswith('/xml'): + if self.contentparams['type'].endswith(u'/xml'): return 0 return 1 def _itsAnHrefDamnIt(self, attrsD): - href = attrsD.get('url', attrsD.get('uri', attrsD.get('href', - None))) + href = attrsD.get('url', attrsD.get('uri', attrsD.get('href', None))) if href: try: del attrsD['url'] except KeyError: pass @@ -1197,40 +1045,39 @@ except KeyError: pass attrsD['href'] = href return attrsD - def _save(self, key, value): + def _save(self, key, value, overwrite=False): context = self._getContext() - context.setdefault(key, value) + if overwrite: + context[key] = value + else: + context.setdefault(key, value) def _start_rss(self, attrsD): - versionmap = { - '0.91': 'rss091u', - '0.92': 'rss092', - '0.93': 'rss093', - '0.94': 'rss094', - } - if not self.version: + versionmap = {'0.91': u'rss091u', + '0.92': u'rss092', + '0.93': u'rss093', + '0.94': u'rss094'} + #If we're here then this is an RSS feed. + #If we don't have a version or have a version that starts with something + #other than RSS then there's been a mistake. Correct it. + if not self.version or not self.version.startswith(u'rss'): attr_version = attrsD.get('version', '') version = versionmap.get(attr_version) if version: self.version = version elif attr_version.startswith('2.'): - self.version = 'rss20' + self.version = u'rss20' else: - self.version = 'rss' - - def _start_dlhottitles(self, attrsD): - self.version = 'hotrss' + self.version = u'rss' def _start_channel(self, attrsD): self.infeed = 1 self._cdf_common(attrsD) - _start_feedinfo = _start_channel - def _cdf_common(self, attrsD): if attrsD.has_key('lastmod'): self._start_modified({}) self.elementstack[-1][-1] = attrsD['lastmod'] self._end_modified() @@ -1239,62 +1086,66 @@ self.elementstack[-1][-1] = attrsD['href'] self._end_link() def _start_feed(self, attrsD): self.infeed = 1 - versionmap = {'0.1': 'atom01', '0.2': 'atom02', '0.3': 'atom03'} + versionmap = {'0.1': u'atom01', + '0.2': u'atom02', + '0.3': u'atom03'} if not self.version: attr_version = attrsD.get('version') version = versionmap.get(attr_version) if version: self.version = version else: - self.version = 'atom' + self.version = u'atom' def _end_channel(self): self.infeed = 0 - _end_feed = _end_channel def _start_image(self, attrsD): + context = self._getContext() + if not self.inentry: + context.setdefault('image', FeedParserDict()) self.inimage = 1 + self.hasTitle = 0 self.push('image', 0) - context = self._getContext() - context.setdefault('image', FeedParserDict()) def _end_image(self): self.pop('image') self.inimage = 0 def _start_textinput(self, attrsD): - self.intextinput = 1 - self.push('textinput', 0) context = self._getContext() context.setdefault('textinput', FeedParserDict()) - + self.intextinput = 1 + self.hasTitle = 0 + self.push('textinput', 0) _start_textInput = _start_textinput def _end_textinput(self): self.pop('textinput') self.intextinput = 0 - _end_textInput = _end_textinput def _start_author(self, attrsD): self.inauthor = 1 self.push('author', 1) - + # Append a new FeedParserDict when expecting an author + context = self._getContext() + context.setdefault('authors', []) + context['authors'].append(FeedParserDict()) _start_managingeditor = _start_author _start_dc_author = _start_author _start_dc_creator = _start_author _start_itunes_author = _start_author def _end_author(self): self.pop('author') self.inauthor = 0 self._sync_author_detail() - _end_managingeditor = _end_author _end_dc_author = _end_author _end_dc_creator = _end_author _end_itunes_author = _end_author @@ -1329,11 +1180,10 @@ self._end_name() self.incontributor = 0 def _start_name(self, attrsD): self.push('name', 0) - _start_itunes_name = _start_name def _end_name(self): value = self.pop('name') if self.inpublisher: @@ -1342,65 +1192,55 @@ self._save_author('name', value) elif self.incontributor: self._save_contributor('name', value) elif self.intextinput: context = self._getContext() - context['textinput']['name'] = value - + context['name'] = value _end_itunes_name = _end_name def _start_width(self, attrsD): self.push('width', 0) def _end_width(self): value = self.pop('width') try: value = int(value) - except: + except ValueError: value = 0 if self.inimage: context = self._getContext() - context['image']['width'] = value + context['width'] = value def _start_height(self, attrsD): self.push('height', 0) def _end_height(self): value = self.pop('height') try: value = int(value) - except: + except ValueError: value = 0 if self.inimage: context = self._getContext() - context['image']['height'] = value + context['height'] = value def _start_url(self, attrsD): self.push('href', 1) - _start_homepage = _start_url _start_uri = _start_url def _end_url(self): value = self.pop('href') if self.inauthor: self._save_author('href', value) elif self.incontributor: self._save_contributor('href', value) - elif self.inimage: - context = self._getContext() - context['image']['href'] = value - elif self.intextinput: - context = self._getContext() - context['textinput']['link'] = value - _end_homepage = _end_url _end_uri = _end_url def _start_email(self, attrsD): self.push('email', 0) - _start_itunes_email = _start_email def _end_email(self): value = self.pop('email') if self.inpublisher: @@ -1407,32 +1247,32 @@ self._save_author('email', value, 'publisher') elif self.inauthor: self._save_author('email', value) elif self.incontributor: self._save_contributor('email', value) - _end_itunes_email = _end_email def _getContext(self): if self.insource: context = self.sourcedata + elif self.inimage and self.feeddata.has_key('image'): + context = self.feeddata['image'] + elif self.intextinput: + context = self.feeddata['textinput'] elif self.inentry: context = self.entries[-1] else: context = self.feeddata return context - def _save_author( - self, - key, - value, - prefix='author', - ): + def _save_author(self, key, value, prefix='author'): context = self._getContext() context.setdefault(prefix + '_detail', FeedParserDict()) context[prefix + '_detail'][key] = value self._sync_author_detail() + context.setdefault('authors', [FeedParserDict()]) + context['authors'][-1][key] = value def _save_contributor(self, key, value): context = self._getContext() context.setdefault('contributors', [FeedParserDict()]) context['contributors'][-1][key] = value @@ -1442,201 +1282,198 @@ detail = context.get('%s_detail' % key) if detail: name = detail.get('name') email = detail.get('email') if name and email: - context[key] = '%s (%s)' % (name, email) + context[key] = u'%s (%s)' % (name, email) elif name: context[key] = name elif email: context[key] = email else: - author = context.get(key) + author, email = context.get(key), None if not author: return - emailmatch = \ - re.search(r'''(([a-zA-Z0-9\_\-\.\+]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?))''' - , author) - if not emailmatch: - return - email = emailmatch.group(0) - - # probably a better way to do the following, but it passes all the tests - - author = author.replace(email, '') - author = author.replace('()', '') - author = author.strip() - if author and author[0] == '(': - author = author[1:] - if author and author[-1] == ')': - author = author[:-1] - author = author.strip() - context.setdefault('%s_detail' % key, FeedParserDict()) - context['%s_detail' % key]['name'] = author - context['%s_detail' % key]['email'] = email + emailmatch = re.search(ur'''(([a-zA-Z0-9\_\-\.\+]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?))(\?subject=\S+)?''', author) + if emailmatch: + email = emailmatch.group(0) + # probably a better way to do the following, but it passes all the tests + author = author.replace(email, u'') + author = author.replace(u'()', u'') + author = author.replace(u'<>', u'') + author = author.replace(u'<>', u'') + author = author.strip() + if author and (author[0] == u'('): + author = author[1:] + if author and (author[-1] == u')'): + author = author[:-1] + author = author.strip() + if author or email: + context.setdefault('%s_detail' % key, FeedParserDict()) + if author: + context['%s_detail' % key]['name'] = author + if email: + context['%s_detail' % key]['email'] = email def _start_subtitle(self, attrsD): - self.pushContent('subtitle', attrsD, 'text/plain', 1) - + self.pushContent('subtitle', attrsD, u'text/plain', 1) _start_tagline = _start_subtitle _start_itunes_subtitle = _start_subtitle def _end_subtitle(self): self.popContent('subtitle') - _end_tagline = _end_subtitle _end_itunes_subtitle = _end_subtitle def _start_rights(self, attrsD): - self.pushContent('rights', attrsD, 'text/plain', 1) - + self.pushContent('rights', attrsD, u'text/plain', 1) _start_dc_rights = _start_rights _start_copyright = _start_rights def _end_rights(self): self.popContent('rights') - _end_dc_rights = _end_rights _end_copyright = _end_rights def _start_item(self, attrsD): self.entries.append(FeedParserDict()) self.push('item', 0) self.inentry = 1 self.guidislink = 0 + self.hasTitle = 0 id = self._getAttribute(attrsD, 'rdf:about') if id: context = self._getContext() context['id'] = id self._cdf_common(attrsD) - _start_entry = _start_item - _start_product = _start_item def _end_item(self): self.pop('item') self.inentry = 0 - _end_entry = _end_item def _start_dc_language(self, attrsD): self.push('language', 1) - _start_language = _start_dc_language def _end_dc_language(self): self.lang = self.pop('language') - _end_language = _end_dc_language def _start_dc_publisher(self, attrsD): self.push('publisher', 1) - _start_webmaster = _start_dc_publisher def _end_dc_publisher(self): self.pop('publisher') self._sync_author_detail('publisher') - _end_webmaster = _end_dc_publisher def _start_published(self, attrsD): self.push('published', 1) - _start_dcterms_issued = _start_published _start_issued = _start_published def _end_published(self): value = self.pop('published') - self._save('published_parsed', _parse_date(value)) - + self._save('published_parsed', _parse_date(value), overwrite=True) _end_dcterms_issued = _end_published _end_issued = _end_published def _start_updated(self, attrsD): self.push('updated', 1) - _start_modified = _start_updated _start_dcterms_modified = _start_updated _start_pubdate = _start_updated _start_dc_date = _start_updated + _start_lastbuilddate = _start_updated def _end_updated(self): value = self.pop('updated') parsed_value = _parse_date(value) - self._save('updated_parsed', parsed_value) - + self._save('updated_parsed', parsed_value, overwrite=True) _end_modified = _end_updated _end_dcterms_modified = _end_updated _end_pubdate = _end_updated _end_dc_date = _end_updated + _end_lastbuilddate = _end_updated def _start_created(self, attrsD): self.push('created', 1) - _start_dcterms_created = _start_created def _end_created(self): value = self.pop('created') - self._save('created_parsed', _parse_date(value)) - + self._save('created_parsed', _parse_date(value), overwrite=True) _end_dcterms_created = _end_created def _start_expirationdate(self, attrsD): self.push('expired', 1) def _end_expirationdate(self): - self._save('expired_parsed', _parse_date(self.pop('expired'))) + self._save('expired_parsed', _parse_date(self.pop('expired')), overwrite=True) def _start_cc_license(self, attrsD): - self.push('license', 1) + context = self._getContext() value = self._getAttribute(attrsD, 'rdf:resource') + attrsD = FeedParserDict() + attrsD['rel'] = u'license' if value: - self.elementstack[-1][2].append(value) - self.pop('license') + attrsD['href']=value + context.setdefault('links', []).append(attrsD) def _start_creativecommons_license(self, attrsD): self.push('license', 1) + _start_creativeCommons_license = _start_creativecommons_license def _end_creativecommons_license(self): - self.pop('license') - - def _addTag( - self, - term, - scheme, - label, - ): + value = self.pop('license') + context = self._getContext() + attrsD = FeedParserDict() + attrsD['rel'] = u'license' + if value: + attrsD['href'] = value + context.setdefault('links', []).append(attrsD) + del context['license'] + _end_creativeCommons_license = _end_creativecommons_license + + def _addXFN(self, relationships, href, name): + context = self._getContext() + xfn = context.setdefault('xfn', []) + value = FeedParserDict({'relationships': relationships, 'href': href, 'name': name}) + if value not in xfn: + xfn.append(value) + + def _addTag(self, term, scheme, label): context = self._getContext() tags = context.setdefault('tags', []) - if not term and not scheme and not label: + if (not term) and (not scheme) and (not label): return - value = FeedParserDict({'term': term, 'scheme': scheme, 'label' - : label}) + value = FeedParserDict({'term': term, 'scheme': scheme, 'label': label}) if value not in tags: - tags.append(FeedParserDict({'term': term, 'scheme': scheme, - 'label': label})) + tags.append(value) def _start_category(self, attrsD): - if _debug: - sys.stderr.write('entering _start_category with %s\n' - % repr(attrsD)) term = attrsD.get('term') scheme = attrsD.get('scheme', attrsD.get('domain')) label = attrsD.get('label') self._addTag(term, scheme, label) self.push('category', 1) - _start_dc_subject = _start_category _start_keywords = _start_category + def _start_media_category(self, attrsD): + attrsD.setdefault('scheme', u'http://search.yahoo.com/mrss/category_schema') + self._start_category(attrsD) + def _end_itunes_keywords(self): for term in self.pop('itunes_keywords').split(): - self._addTag(term, 'http://www.itunes.com/', None) + self._addTag(term, u'http://www.itunes.com/', None) def _start_itunes_category(self, attrsD): - self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None) + self._addTag(attrsD.get('text'), u'http://www.itunes.com/', None) self.push('category', 1) def _end_category(self): value = self.pop('category') if not value: @@ -1645,120 +1482,104 @@ tags = context['tags'] if value and len(tags) and not tags[-1]['term']: tags[-1]['term'] = value else: self._addTag(value, None, None) - _end_dc_subject = _end_category _end_keywords = _end_category _end_itunes_category = _end_category + _end_media_category = _end_category def _start_cloud(self, attrsD): self._getContext()['cloud'] = FeedParserDict(attrsD) def _start_link(self, attrsD): - attrsD.setdefault('rel', 'alternate') - attrsD.setdefault('type', 'text/html') + attrsD.setdefault('rel', u'alternate') + if attrsD['rel'] == u'self': + attrsD.setdefault('type', u'application/atom+xml') + else: + attrsD.setdefault('type', u'text/html') + context = self._getContext() attrsD = self._itsAnHrefDamnIt(attrsD) if attrsD.has_key('href'): attrsD['href'] = self.resolveURI(attrsD['href']) expectingText = self.infeed or self.inentry or self.insource - context = self._getContext() context.setdefault('links', []) - context['links'].append(FeedParserDict(attrsD)) - if attrsD['rel'] == 'enclosure': - self._start_enclosure(attrsD) + if not (self.inentry and self.inimage): + context['links'].append(FeedParserDict(attrsD)) if attrsD.has_key('href'): expectingText = 0 - if attrsD.get('rel') == 'alternate'\ - and self.mapContentType(attrsD.get('type'))\ - in self.html_types: + if (attrsD.get('rel') == u'alternate') and (self.mapContentType(attrsD.get('type')) in self.html_types): context['link'] = attrsD['href'] else: self.push('link', expectingText) - _start_producturl = _start_link - def _end_link(self): value = self.pop('link') context = self._getContext() - if self.intextinput: - context['textinput']['link'] = value - if self.inimage: - context['image']['link'] = value - - _end_producturl = _end_link def _start_guid(self, attrsD): - self.guidislink = attrsD.get('ispermalink', 'true') == 'true' + self.guidislink = (attrsD.get('ispermalink', 'true') == 'true') self.push('id', 1) def _end_guid(self): value = self.pop('id') - self._save('guidislink', self.guidislink - and not self._getContext().has_key('link')) + self._save('guidislink', self.guidislink and not self._getContext().has_key('link')) if self.guidislink: - # guid acts as link, but only if 'ispermalink' is not present or is 'true', # and only if the item doesn't already have a link element - self._save('link', value) def _start_title(self, attrsD): - self.pushContent('title', attrsD, 'text/plain', self.infeed - or self.inentry or self.insource) - + if self.svgOK: + return self.unknown_starttag('title', attrsD.items()) + self.pushContent('title', attrsD, u'text/plain', self.infeed or self.inentry or self.insource) _start_dc_title = _start_title _start_media_title = _start_title def _end_title(self): + if self.svgOK: + return value = self.popContent('title') + if not value: + return context = self._getContext() - if self.intextinput: - context['textinput']['title'] = value - elif self.inimage: - context['image']['title'] = value - + self.hasTitle = 1 _end_dc_title = _end_title - _end_media_title = _end_title + + def _end_media_title(self): + hasTitle = self.hasTitle + self._end_title() + self.hasTitle = hasTitle def _start_description(self, attrsD): context = self._getContext() if context.has_key('summary'): self._summaryKey = 'content' self._start_content(attrsD) else: - self.pushContent('description', attrsD, 'text/html', - self.infeed or self.inentry - or self.insource) + self.pushContent('description', attrsD, u'text/html', self.infeed or self.inentry or self.insource) + _start_dc_description = _start_description def _start_abstract(self, attrsD): - self.pushContent('description', attrsD, 'text/plain', - self.infeed or self.inentry or self.insource) + self.pushContent('description', attrsD, u'text/plain', self.infeed or self.inentry or self.insource) def _end_description(self): if self._summaryKey == 'content': self._end_content() else: value = self.popContent('description') - context = self._getContext() - if self.intextinput: - context['textinput']['description'] = value - elif self.inimage: - context['image']['description'] = value self._summaryKey = None - _end_abstract = _end_description + _end_dc_description = _end_description def _start_info(self, attrsD): - self.pushContent('info', attrsD, 'text/plain', 1) - + self.pushContent('info', attrsD, u'text/plain', 1) _start_feedburner_browserfriendly = _start_info def _end_info(self): self.popContent('info') - _end_feedburner_browserfriendly = _end_info def _start_generator(self, attrsD): if attrsD: attrsD = self._itsAnHrefDamnIt(attrsD) @@ -1777,12 +1598,11 @@ self.push('generator', 1) value = self._getAttribute(attrsD, 'rdf:resource') if value: self.elementstack[-1][2].append(value) self.pop('generator') - self._getContext()['generator_detail'] = FeedParserDict({'href' - : value}) + self._getContext()['generator_detail'] = FeedParserDict({'href': value}) def _start_admin_errorreportsto(self, attrsD): self.push('errorreportsto', 1) value = self._getAttribute(attrsD, 'rdf:resource') if value: @@ -1794,189 +1614,207 @@ if context.has_key('summary'): self._summaryKey = 'content' self._start_content(attrsD) else: self._summaryKey = 'summary' - self.pushContent(self._summaryKey, attrsD, 'text/plain', 1) - + self.pushContent(self._summaryKey, attrsD, u'text/plain', 1) _start_itunes_summary = _start_summary def _end_summary(self): if self._summaryKey == 'content': self._end_content() else: self.popContent(self._summaryKey or 'summary') self._summaryKey = None - _end_itunes_summary = _end_summary def _start_enclosure(self, attrsD): attrsD = self._itsAnHrefDamnIt(attrsD) - self._getContext().setdefault('enclosures', - []).append(FeedParserDict(attrsD)) - href = attrsD.get('href') - if href: - context = self._getContext() - if not context.get('id'): - context['id'] = href + context = self._getContext() + attrsD['rel'] = u'enclosure' + context.setdefault('links', []).append(FeedParserDict(attrsD)) def _start_source(self, attrsD): + if 'url' in attrsD: + # This means that we're processing a source element from an RSS 2.0 feed + self.sourcedata['href'] = attrsD[u'url'] + self.push('source', 1) self.insource = 1 + self.hasTitle = 0 def _end_source(self): self.insource = 0 + value = self.pop('source') + if value: + self.sourcedata['title'] = value self._getContext()['source'] = copy.deepcopy(self.sourcedata) self.sourcedata.clear() def _start_content(self, attrsD): - self.pushContent('content', attrsD, 'text/plain', 1) + self.pushContent('content', attrsD, u'text/plain', 1) src = attrsD.get('src') if src: self.contentparams['src'] = src self.push('content', 1) - def _start_prodlink(self, attrsD): - self.pushContent('content', attrsD, 'text/html', 1) - def _start_body(self, attrsD): - self.pushContent('content', attrsD, 'application/xhtml+xml', 1) - + self.pushContent('content', attrsD, u'application/xhtml+xml', 1) _start_xhtml_body = _start_body def _start_content_encoded(self, attrsD): - self.pushContent('content', attrsD, 'text/html', 1) - + self.pushContent('content', attrsD, u'text/html', 1) _start_fullitem = _start_content_encoded def _end_content(self): - copyToDescription = \ - self.mapContentType(self.contentparams.get('type'))\ - in ['text/plain'] + self.html_types + copyToSummary = self.mapContentType(self.contentparams.get('type')) in ([u'text/plain'] + self.html_types) value = self.popContent('content') - if copyToDescription: - self._save('description', value) + if copyToSummary: + self._save('summary', value) _end_body = _end_content _end_xhtml_body = _end_content _end_content_encoded = _end_content _end_fullitem = _end_content - _end_prodlink = _end_content def _start_itunes_image(self, attrsD): self.push('itunes_image', 0) - self._getContext()['image'] = FeedParserDict({'href' - : attrsD.get('href')}) - + if attrsD.get('href'): + self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')}) _start_itunes_link = _start_itunes_image def _end_itunes_block(self): value = self.pop('itunes_block', 0) - self._getContext()['itunes_block'] = value == 'yes' and 1 or 0 + self._getContext()['itunes_block'] = (value == 'yes') and 1 or 0 def _end_itunes_explicit(self): value = self.pop('itunes_explicit', 0) - self._getContext()['itunes_explicit'] = value == 'yes' and 1\ - or 0 + # Convert 'yes' -> True, 'clean' to False, and any other value to None + # False and None both evaluate as False, so the difference can be ignored + # by applications that only need to know if the content is explicit. + self._getContext()['itunes_explicit'] = (None, False, True)[(value == 'yes' and 2) or value == 'clean' or 0] + def _start_media_content(self, attrsD): + context = self._getContext() + context.setdefault('media_content', []) + context['media_content'].append(attrsD) + + def _start_media_thumbnail(self, attrsD): + context = self._getContext() + context.setdefault('media_thumbnail', []) + self.push('url', 1) # new + context['media_thumbnail'].append(attrsD) + + def _end_media_thumbnail(self): + url = self.pop('url') + context = self._getContext() + if url != None and len(url.strip()) != 0: + if not context['media_thumbnail'][-1].has_key('url'): + context['media_thumbnail'][-1]['url'] = url + + def _start_media_player(self, attrsD): + self.push('media_player', 0) + self._getContext()['media_player'] = FeedParserDict(attrsD) + + def _end_media_player(self): + value = self.pop('media_player') + context = self._getContext() + context['media_player']['content'] = value + + def _start_newlocation(self, attrsD): + self.push('newlocation', 1) + + def _end_newlocation(self): + url = self.pop('newlocation') + context = self._getContext() + # don't set newlocation if the context isn't right + if context is not self.feeddata: + return + context['newlocation'] = _makeSafeAbsoluteURI(self.baseuri, url.strip()) if _XML_AVAILABLE: - - - class _StrictFeedParser(_FeedParserMixin, - xml.sax.handler.ContentHandler): - - def __init__( - self, - baseuri, - baselang, - encoding, - ): - if _debug: - sys.stderr.write('trying StrictFeedParser\n') + class _StrictFeedParser(_FeedParserMixin, xml.sax.handler.ContentHandler): + def __init__(self, baseuri, baselang, encoding): xml.sax.handler.ContentHandler.__init__(self) _FeedParserMixin.__init__(self, baseuri, baselang, encoding) self.bozo = 0 self.exc = None + self.decls = {} def startPrefixMapping(self, prefix, uri): + if not uri: + return + # Jython uses '' instead of None; standardize on None + prefix = prefix or None self.trackNamespace(prefix, uri) - - def startElementNS( - self, - name, - qname, - attrs, - ): - (namespace, localname) = name + if prefix and uri == 'http://www.w3.org/1999/xlink': + self.decls['xmlns:' + prefix] = uri + + def startElementNS(self, name, qname, attrs): + namespace, localname = name lowernamespace = str(namespace or '').lower() - if lowernamespace.find('backend.userland.com/rss') != -1: - + if lowernamespace.find(u'backend.userland.com/rss') <> -1: # match any backend.userland.com namespace - - namespace = 'http://backend.userland.com/rss' + namespace = u'http://backend.userland.com/rss' lowernamespace = namespace if qname and qname.find(':') > 0: givenprefix = qname.split(':')[0] else: givenprefix = None - prefix = self._matchnamespaces.get(lowernamespace, - givenprefix) - if givenprefix and (prefix == None or prefix == '' - and lowernamespace == '')\ - and not self.namespacesInUse.has_key(givenprefix): - raise UndeclaredNamespace, \ - "'%s' is not associated with a namespace"\ - % givenprefix - if prefix: - localname = prefix + ':' + localname + prefix = self._matchnamespaces.get(lowernamespace, givenprefix) + if givenprefix and (prefix == None or (prefix == '' and lowernamespace == '')) and not self.namespacesInUse.has_key(givenprefix): + raise UndeclaredNamespace, "'%s' is not associated with a namespace" % givenprefix localname = str(localname).lower() - if _debug: - sys.stderr.write('startElementNS: qname = %s, namespace = %s, givenprefix = %s, prefix = %s, attrs = %s, localname = %s\n' - % ( - qname, - namespace, - givenprefix, - prefix, - attrs.items(), - localname, - )) # qname implementation is horribly broken in Python 2.1 (it # doesn't report any), and slightly broken in Python 2.2 (it # doesn't report the xml: namespace). So we match up namespaces # with a known list first, and then possibly override them with # the qnames the SAX parser gives us (if indeed it gives us any # at all). Thanks to MatejC for helping me test this and # tirelessly telling me that it didn't work yet. + attrsD, self.decls = self.decls, {} + if localname=='math' and namespace=='http://www.w3.org/1998/Math/MathML': + attrsD['xmlns']=namespace + if localname=='svg' and namespace=='http://www.w3.org/2000/svg': + attrsD['xmlns']=namespace - attrsD = {} - for ((namespace, attrlocalname), attrvalue) in \ - attrs._attrs.items(): + if prefix: + localname = prefix.lower() + ':' + localname + elif namespace and not qname: #Expat + for name,value in self.namespacesInUse.items(): + if name and value == namespace: + localname = name + ':' + localname + break + + for (namespace, attrlocalname), attrvalue in attrs.items(): lowernamespace = (namespace or '').lower() prefix = self._matchnamespaces.get(lowernamespace, '') if prefix: attrlocalname = prefix + ':' + attrlocalname attrsD[str(attrlocalname).lower()] = attrvalue for qname in attrs.getQNames(): - attrsD[str(qname).lower()] = \ - attrs.getValueByQName(qname) + attrsD[str(qname).lower()] = attrs.getValueByQName(qname) self.unknown_starttag(localname, attrsD.items()) def characters(self, text): self.handle_data(text) def endElementNS(self, name, qname): - (namespace, localname) = name + namespace, localname = name lowernamespace = str(namespace or '').lower() if qname and qname.find(':') > 0: givenprefix = qname.split(':')[0] else: givenprefix = '' - prefix = self._matchnamespaces.get(lowernamespace, - givenprefix) + prefix = self._matchnamespaces.get(lowernamespace, givenprefix) if prefix: localname = prefix + ':' + localname + elif namespace and not qname: #Expat + for name,value in self.namespacesInUse.items(): + if name and value == namespace: + localname = name + ':' + localname + break localname = str(localname).lower() self.unknown_endtag(localname) def error(self, exc): self.bozo = 1 @@ -1984,34 +1822,22 @@ def fatalError(self, exc): self.error(exc) raise exc - class _BaseHTMLProcessor(sgmllib.SGMLParser): - + special = re.compile('''[<>'"]''') + bare_ampersand = re.compile("&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") elements_no_end_tag = [ - 'area', - 'base', - 'basefont', - 'br', - 'col', - 'frame', - 'hr', - 'img', - 'input', - 'isindex', - 'link', - 'meta', - 'param', - ] - - def __init__(self, encoding): + 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', + 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', + 'source', 'track', 'wbr' + ] + + def __init__(self, encoding, _type): self.encoding = encoding - if _debug: - sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' - % self.encoding) + self._type = _type sgmllib.SGMLParser.__init__(self) def reset(self): self.pieces = [] sgmllib.SGMLParser.reset(self) @@ -2020,398 +1846,911 @@ tag = match.group(1) if tag in self.elements_no_end_tag: return '<' + tag + ' />' else: return '<' + tag + '>' + + # By declaring these methods and overriding their compiled code + # with the code from sgmllib, the original code will execute in + # feedparser's scope instead of sgmllib's. This means that the + # `tagfind` and `charref` regular expressions will be found as + # they're declared above, not as they're declared in sgmllib. + def goahead(self, i): + pass + goahead.func_code = sgmllib.SGMLParser.goahead.func_code + + def __parse_starttag(self, i): + pass + __parse_starttag.func_code = sgmllib.SGMLParser.parse_starttag.func_code + + def parse_starttag(self,i): + j = self.__parse_starttag(i) + if self._type == 'application/xhtml+xml': + if j>2 and self.rawdata[j-2:j]=='/>': + self.unknown_endtag(self.lasttag) + return j def feed(self, data): - data = re.compile(r'', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace - - data = re.sub(r'<([^<\s]+?)\s*/>', self._shorttag_replace, data) + data = re.compile(r'', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace + data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data) data = data.replace(''', "'") data = data.replace('"', '"') - if self.encoding and type(data) == type(u''): - data = data.encode(self.encoding) + try: + bytes + if bytes is str: + raise NameError + self.encoding = self.encoding + u'_INVALID_PYTHON_3' + except NameError: + if self.encoding and isinstance(data, unicode): + data = data.encode(self.encoding) sgmllib.SGMLParser.feed(self, data) + sgmllib.SGMLParser.close(self) def normalize_attrs(self, attrs): - + if not attrs: + return attrs # utility method to be called by descendants - - attrs = [(k.lower(), v) for (k, v) in attrs] - attrs = [(k, k in ('rel', 'type') and v.lower() or v) for (k, - v) in attrs] + attrs = dict([(k.lower(), v) for k, v in attrs]).items() + attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs] + attrs.sort() return attrs def unknown_starttag(self, tag, attrs): - # called for each start tag # attrs is a list of (attr, value) tuples # e.g. for
    , tag='pre', attrs=[('class', 'screen')]
    -
    -        if _debug:
    -            sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n'
    -                              % tag)
             uattrs = []
    -
    -        # thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds
    -
    -        for (key, value) in attrs:
    -            if type(value) != type(u''):
    -                value = unicode(value, self.encoding)
    -            uattrs.append((unicode(key, self.encoding), value))
    -        strattrs = u''.join([u' %s="%s"' % (key, value) for (key,
    -                            value) in uattrs]).encode(self.encoding)
    +        strattrs=''
    +        if attrs:
    +            for key, value in attrs:
    +                value=value.replace('>','>').replace('<','<').replace('"','"')
    +                value = self.bare_ampersand.sub("&", value)
    +                # thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds
    +                if not isinstance(value, unicode):
    +                    value = value.decode(self.encoding, 'ignore')
    +                try:
    +                    # Currently, in Python 3 the key is already a str, and cannot be decoded again
    +                    uattrs.append((unicode(key, self.encoding), value))
    +                except TypeError:
    +                    uattrs.append((key, value))
    +            strattrs = u''.join([u' %s="%s"' % (key, value) for key, value in uattrs])
    +            if self.encoding:
    +                try:
    +                    strattrs = strattrs.encode(self.encoding)
    +                except (UnicodeEncodeError, LookupError):
    +                    pass
             if tag in self.elements_no_end_tag:
                 self.pieces.append('<%(tag)s%(strattrs)s />' % locals())
             else:
                 self.pieces.append('<%(tag)s%(strattrs)s>' % locals())
     
         def unknown_endtag(self, tag):
    -
             # called for each end tag, e.g. for 
    , tag will be 'pre' # Reconstruct the original end tag. - if tag not in self.elements_no_end_tag: - self.pieces.append('' % locals()) + self.pieces.append("" % locals()) def handle_charref(self, ref): - # called for each character reference, e.g. for ' ', ref will be '160' # Reconstruct the original character reference. + if ref.startswith('x'): + value = unichr(int(ref[1:],16)) + else: + value = unichr(int(ref)) - self.pieces.append('&#%(ref)s;' % locals()) + if value in _cp1252.keys(): + self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:]) + else: + self.pieces.append('&#%(ref)s;' % locals()) def handle_entityref(self, ref): - # called for each entity reference, e.g. for '©', ref will be 'copy' # Reconstruct the original entity reference. - - self.pieces.append('&%(ref)s;' % locals()) + if name2codepoint.has_key(ref): + self.pieces.append('&%(ref)s;' % locals()) + else: + self.pieces.append('&%(ref)s' % locals()) def handle_data(self, text): - # called for each block of plain text, i.e. outside of any tag and # not containing any character or entity references # Store the original text verbatim. - - if _debug: - sys.stderr.write('_BaseHTMLProcessor, handle_text, text=%s\n' - % text) self.pieces.append(text) def handle_comment(self, text): - # called for each HTML comment, e.g. # Reconstruct the original comment. - self.pieces.append('' % locals()) def handle_pi(self, text): - # called for each processing instruction, e.g. # Reconstruct original processing instruction. - self.pieces.append('' % locals()) def handle_decl(self, text): - # called for the DOCTYPE, if present, e.g. # # Reconstruct original DOCTYPE - self.pieces.append('' % locals()) - _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*' - ).match - + _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match def _scan_name(self, i, declstartpos): rawdata = self.rawdata n = len(rawdata) if i == n: - return (None, -1) + return None, -1 m = self._new_declname_match(rawdata, i) if m: s = m.group() name = s.strip() - if i + len(s) == n: - return (None, -1) # end of buffer - return (name.lower(), m.end()) + if (i + len(s)) == n: + return None, -1 # end of buffer + return name.lower(), m.end() else: self.handle_data(rawdata) - # self.updatepos(declstartpos, i) + return None, -1 - return (None, -1) + def convert_charref(self, name): + return '&#%s;' % name + + def convert_entityref(self, name): + return '&%s;' % name def output(self): - """Return processed HTML as a single string""" - + '''Return processed HTML as a single string''' return ''.join([str(p) for p in self.pieces]) + def parse_declaration(self, i): + try: + return sgmllib.SGMLParser.parse_declaration(self, i) + except sgmllib.SGMLParseError: + # escape the doctype declaration and continue parsing + self.handle_data('<') + return i+1 class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor): - - def __init__( - self, - baseuri, - baselang, - encoding, - ): + def __init__(self, baseuri, baselang, encoding, entities): sgmllib.SGMLParser.__init__(self) _FeedParserMixin.__init__(self, baseuri, baselang, encoding) + _BaseHTMLProcessor.__init__(self, encoding, 'application/xhtml+xml') + self.entities=entities def decodeEntities(self, element, data): data = data.replace('<', '<') data = data.replace('<', '<') + data = data.replace('<', '<') data = data.replace('>', '>') data = data.replace('>', '>') + data = data.replace('>', '>') data = data.replace('&', '&') data = data.replace('&', '&') data = data.replace('"', '"') data = data.replace('"', '"') data = data.replace(''', ''') data = data.replace(''', ''') - if self.contentparams.has_key('type')\ - and not self.contentparams.get('type', 'xml' - ).endswith('xml'): + if self.contentparams.has_key('type') and not self.contentparams.get('type', u'xml').endswith(u'xml'): data = data.replace('<', '<') data = data.replace('>', '>') data = data.replace('&', '&') data = data.replace('"', '"') data = data.replace(''', "'") return data + def strattrs(self, attrs): + return ''.join([' %s="%s"' % (n,v.replace('"','"')) for n,v in attrs]) + +class _MicroformatsParser: + STRING = 1 + DATE = 2 + URI = 3 + NODE = 4 + EMAIL = 5 + + known_xfn_relationships = ['contact', 'acquaintance', 'friend', 'met', 'co-worker', 'coworker', 'colleague', 'co-resident', 'coresident', 'neighbor', 'child', 'parent', 'sibling', 'brother', 'sister', 'spouse', 'wife', 'husband', 'kin', 'relative', 'muse', 'crush', 'date', 'sweetheart', 'me'] + known_binary_extensions = ['zip','rar','exe','gz','tar','tgz','tbz2','bz2','z','7z','dmg','img','sit','sitx','hqx','deb','rpm','bz2','jar','rar','iso','bin','msi','mp2','mp3','ogg','ogm','mp4','m4v','m4a','avi','wma','wmv'] + + def __init__(self, data, baseuri, encoding): + self.document = BeautifulSoup.BeautifulSoup(data) + self.baseuri = baseuri + self.encoding = encoding + if isinstance(data, unicode): + data = data.encode(encoding) + self.tags = [] + self.enclosures = [] + self.xfn = [] + self.vcard = None + + def vcardEscape(self, s): + if isinstance(s, basestring): + s = s.replace(',', '\\,').replace(';', '\\;').replace('\n', '\\n') + return s + + def vcardFold(self, s): + s = re.sub(';+$', '', s) + sFolded = '' + iMax = 75 + sPrefix = '' + while len(s) > iMax: + sFolded += sPrefix + s[:iMax] + '\n' + s = s[iMax:] + sPrefix = ' ' + iMax = 74 + sFolded += sPrefix + s + return sFolded + + def normalize(self, s): + return re.sub(r'\s+', ' ', s).strip() + + def unique(self, aList): + results = [] + for element in aList: + if element not in results: + results.append(element) + return results + + def toISO8601(self, dt): + return time.strftime('%Y-%m-%dT%H:%M:%SZ', dt) + + def getPropertyValue(self, elmRoot, sProperty, iPropertyType=4, bAllowMultiple=0, bAutoEscape=0): + all = lambda x: 1 + sProperty = sProperty.lower() + bFound = 0 + bNormalize = 1 + propertyMatch = {'class': re.compile(r'\b%s\b' % sProperty)} + if bAllowMultiple and (iPropertyType != self.NODE): + snapResults = [] + containers = elmRoot(['ul', 'ol'], propertyMatch) + for container in containers: + snapResults.extend(container('li')) + bFound = (len(snapResults) != 0) + if not bFound: + snapResults = elmRoot(all, propertyMatch) + bFound = (len(snapResults) != 0) + if (not bFound) and (sProperty == 'value'): + snapResults = elmRoot('pre') + bFound = (len(snapResults) != 0) + bNormalize = not bFound + if not bFound: + snapResults = [elmRoot] + bFound = (len(snapResults) != 0) + arFilter = [] + if sProperty == 'vcard': + snapFilter = elmRoot(all, propertyMatch) + for node in snapFilter: + if node.findParent(all, propertyMatch): + arFilter.append(node) + arResults = [] + for node in snapResults: + if node not in arFilter: + arResults.append(node) + bFound = (len(arResults) != 0) + if not bFound: + if bAllowMultiple: + return [] + elif iPropertyType == self.STRING: + return '' + elif iPropertyType == self.DATE: + return None + elif iPropertyType == self.URI: + return '' + elif iPropertyType == self.NODE: + return None + else: + return None + arValues = [] + for elmResult in arResults: + sValue = None + if iPropertyType == self.NODE: + if bAllowMultiple: + arValues.append(elmResult) + continue + else: + return elmResult + sNodeName = elmResult.name.lower() + if (iPropertyType == self.EMAIL) and (sNodeName == 'a'): + sValue = (elmResult.get('href') or '').split('mailto:').pop().split('?')[0] + if sValue: + sValue = bNormalize and self.normalize(sValue) or sValue.strip() + if (not sValue) and (sNodeName == 'abbr'): + sValue = elmResult.get('title') + if sValue: + sValue = bNormalize and self.normalize(sValue) or sValue.strip() + if (not sValue) and (iPropertyType == self.URI): + if sNodeName == 'a': + sValue = elmResult.get('href') + elif sNodeName == 'img': + sValue = elmResult.get('src') + elif sNodeName == 'object': + sValue = elmResult.get('data') + if sValue: + sValue = bNormalize and self.normalize(sValue) or sValue.strip() + if (not sValue) and (sNodeName == 'img'): + sValue = elmResult.get('alt') + if sValue: + sValue = bNormalize and self.normalize(sValue) or sValue.strip() + if not sValue: + sValue = elmResult.renderContents() + sValue = re.sub(r'<\S[^>]*>', '', sValue) + sValue = sValue.replace('\r\n', '\n') + sValue = sValue.replace('\r', '\n') + if sValue: + sValue = bNormalize and self.normalize(sValue) or sValue.strip() + if not sValue: + continue + if iPropertyType == self.DATE: + sValue = _parse_date_iso8601(sValue) + if bAllowMultiple: + arValues.append(bAutoEscape and self.vcardEscape(sValue) or sValue) + else: + return bAutoEscape and self.vcardEscape(sValue) or sValue + return arValues + + def findVCards(self, elmRoot, bAgentParsing=0): + sVCards = '' + + if not bAgentParsing: + arCards = self.getPropertyValue(elmRoot, 'vcard', bAllowMultiple=1) + else: + arCards = [elmRoot] + + for elmCard in arCards: + arLines = [] + + def processSingleString(sProperty): + sValue = self.getPropertyValue(elmCard, sProperty, self.STRING, bAutoEscape=1).decode(self.encoding) + if sValue: + arLines.append(self.vcardFold(sProperty.upper() + ':' + sValue)) + return sValue or u'' + + def processSingleURI(sProperty): + sValue = self.getPropertyValue(elmCard, sProperty, self.URI) + if sValue: + sContentType = '' + sEncoding = '' + sValueKey = '' + if sValue.startswith('data:'): + sEncoding = ';ENCODING=b' + sContentType = sValue.split(';')[0].split('/').pop() + sValue = sValue.split(',', 1).pop() + else: + elmValue = self.getPropertyValue(elmCard, sProperty) + if elmValue: + if sProperty != 'url': + sValueKey = ';VALUE=uri' + sContentType = elmValue.get('type', '').strip().split('/').pop().strip() + sContentType = sContentType.upper() + if sContentType == 'OCTET-STREAM': + sContentType = '' + if sContentType: + sContentType = ';TYPE=' + sContentType.upper() + arLines.append(self.vcardFold(sProperty.upper() + sEncoding + sContentType + sValueKey + ':' + sValue)) + + def processTypeValue(sProperty, arDefaultType, arForceType=None): + arResults = self.getPropertyValue(elmCard, sProperty, bAllowMultiple=1) + for elmResult in arResults: + arType = self.getPropertyValue(elmResult, 'type', self.STRING, 1, 1) + if arForceType: + arType = self.unique(arForceType + arType) + if not arType: + arType = arDefaultType + sValue = self.getPropertyValue(elmResult, 'value', self.EMAIL, 0) + if sValue: + arLines.append(self.vcardFold(sProperty.upper() + ';TYPE=' + ','.join(arType) + ':' + sValue)) + + # AGENT + # must do this before all other properties because it is destructive + # (removes nested class="vcard" nodes so they don't interfere with + # this vcard's other properties) + arAgent = self.getPropertyValue(elmCard, 'agent', bAllowMultiple=1) + for elmAgent in arAgent: + if re.compile(r'\bvcard\b').search(elmAgent.get('class')): + sAgentValue = self.findVCards(elmAgent, 1) + '\n' + sAgentValue = sAgentValue.replace('\n', '\\n') + sAgentValue = sAgentValue.replace(';', '\\;') + if sAgentValue: + arLines.append(self.vcardFold('AGENT:' + sAgentValue)) + # Completely remove the agent element from the parse tree + elmAgent.extract() + else: + sAgentValue = self.getPropertyValue(elmAgent, 'value', self.URI, bAutoEscape=1); + if sAgentValue: + arLines.append(self.vcardFold('AGENT;VALUE=uri:' + sAgentValue)) + + # FN (full name) + sFN = processSingleString('fn') + + # N (name) + elmName = self.getPropertyValue(elmCard, 'n') + if elmName: + sFamilyName = self.getPropertyValue(elmName, 'family-name', self.STRING, bAutoEscape=1) + sGivenName = self.getPropertyValue(elmName, 'given-name', self.STRING, bAutoEscape=1) + arAdditionalNames = self.getPropertyValue(elmName, 'additional-name', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'additional-names', self.STRING, 1, 1) + arHonorificPrefixes = self.getPropertyValue(elmName, 'honorific-prefix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-prefixes', self.STRING, 1, 1) + arHonorificSuffixes = self.getPropertyValue(elmName, 'honorific-suffix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-suffixes', self.STRING, 1, 1) + arLines.append(self.vcardFold('N:' + sFamilyName + ';' + + sGivenName + ';' + + ','.join(arAdditionalNames) + ';' + + ','.join(arHonorificPrefixes) + ';' + + ','.join(arHonorificSuffixes))) + elif sFN: + # implied "N" optimization + # http://microformats.org/wiki/hcard#Implied_.22N.22_Optimization + arNames = self.normalize(sFN).split() + if len(arNames) == 2: + bFamilyNameFirst = (arNames[0].endswith(',') or + len(arNames[1]) == 1 or + ((len(arNames[1]) == 2) and (arNames[1].endswith('.')))) + if bFamilyNameFirst: + arLines.append(self.vcardFold('N:' + arNames[0] + ';' + arNames[1])) + else: + arLines.append(self.vcardFold('N:' + arNames[1] + ';' + arNames[0])) + + # SORT-STRING + sSortString = self.getPropertyValue(elmCard, 'sort-string', self.STRING, bAutoEscape=1) + if sSortString: + arLines.append(self.vcardFold('SORT-STRING:' + sSortString)) + + # NICKNAME + arNickname = self.getPropertyValue(elmCard, 'nickname', self.STRING, 1, 1) + if arNickname: + arLines.append(self.vcardFold('NICKNAME:' + ','.join(arNickname))) + + # PHOTO + processSingleURI('photo') + + # BDAY + dtBday = self.getPropertyValue(elmCard, 'bday', self.DATE) + if dtBday: + arLines.append(self.vcardFold('BDAY:' + self.toISO8601(dtBday))) + + # ADR (address) + arAdr = self.getPropertyValue(elmCard, 'adr', bAllowMultiple=1) + for elmAdr in arAdr: + arType = self.getPropertyValue(elmAdr, 'type', self.STRING, 1, 1) + if not arType: + arType = ['intl','postal','parcel','work'] # default adr types, see RFC 2426 section 3.2.1 + sPostOfficeBox = self.getPropertyValue(elmAdr, 'post-office-box', self.STRING, 0, 1) + sExtendedAddress = self.getPropertyValue(elmAdr, 'extended-address', self.STRING, 0, 1) + sStreetAddress = self.getPropertyValue(elmAdr, 'street-address', self.STRING, 0, 1) + sLocality = self.getPropertyValue(elmAdr, 'locality', self.STRING, 0, 1) + sRegion = self.getPropertyValue(elmAdr, 'region', self.STRING, 0, 1) + sPostalCode = self.getPropertyValue(elmAdr, 'postal-code', self.STRING, 0, 1) + sCountryName = self.getPropertyValue(elmAdr, 'country-name', self.STRING, 0, 1) + arLines.append(self.vcardFold('ADR;TYPE=' + ','.join(arType) + ':' + + sPostOfficeBox + ';' + + sExtendedAddress + ';' + + sStreetAddress + ';' + + sLocality + ';' + + sRegion + ';' + + sPostalCode + ';' + + sCountryName)) + + # LABEL + processTypeValue('label', ['intl','postal','parcel','work']) + + # TEL (phone number) + processTypeValue('tel', ['voice']) + + # EMAIL + processTypeValue('email', ['internet'], ['internet']) + + # MAILER + processSingleString('mailer') + + # TZ (timezone) + processSingleString('tz') + + # GEO (geographical information) + elmGeo = self.getPropertyValue(elmCard, 'geo') + if elmGeo: + sLatitude = self.getPropertyValue(elmGeo, 'latitude', self.STRING, 0, 1) + sLongitude = self.getPropertyValue(elmGeo, 'longitude', self.STRING, 0, 1) + arLines.append(self.vcardFold('GEO:' + sLatitude + ';' + sLongitude)) + + # TITLE + processSingleString('title') + + # ROLE + processSingleString('role') + + # LOGO + processSingleURI('logo') + + # ORG (organization) + elmOrg = self.getPropertyValue(elmCard, 'org') + if elmOrg: + sOrganizationName = self.getPropertyValue(elmOrg, 'organization-name', self.STRING, 0, 1) + if not sOrganizationName: + # implied "organization-name" optimization + # http://microformats.org/wiki/hcard#Implied_.22organization-name.22_Optimization + sOrganizationName = self.getPropertyValue(elmCard, 'org', self.STRING, 0, 1) + if sOrganizationName: + arLines.append(self.vcardFold('ORG:' + sOrganizationName)) + else: + arOrganizationUnit = self.getPropertyValue(elmOrg, 'organization-unit', self.STRING, 1, 1) + arLines.append(self.vcardFold('ORG:' + sOrganizationName + ';' + ';'.join(arOrganizationUnit))) + + # CATEGORY + arCategory = self.getPropertyValue(elmCard, 'category', self.STRING, 1, 1) + self.getPropertyValue(elmCard, 'categories', self.STRING, 1, 1) + if arCategory: + arLines.append(self.vcardFold('CATEGORIES:' + ','.join(arCategory))) + + # NOTE + processSingleString('note') + + # REV + processSingleString('rev') + + # SOUND + processSingleURI('sound') + + # UID + processSingleString('uid') + + # URL + processSingleURI('url') + + # CLASS + processSingleString('class') + + # KEY + processSingleURI('key') + + if arLines: + arLines = [u'BEGIN:vCard',u'VERSION:3.0'] + arLines + [u'END:vCard'] + # XXX - this is super ugly; properly fix this with issue 148 + for i, s in enumerate(arLines): + if not isinstance(s, unicode): + arLines[i] = s.decode('utf-8', 'ignore') + sVCards += u'\n'.join(arLines) + u'\n' + + return sVCards.strip() + + def isProbablyDownloadable(self, elm): + attrsD = elm.attrMap + if not attrsD.has_key('href'): + return 0 + linktype = attrsD.get('type', '').strip() + if linktype.startswith('audio/') or \ + linktype.startswith('video/') or \ + (linktype.startswith('application/') and not linktype.endswith('xml')): + return 1 + path = urlparse.urlparse(attrsD['href'])[2] + if path.find('.') == -1: + return 0 + fileext = path.split('.').pop().lower() + return fileext in self.known_binary_extensions + + def findTags(self): + all = lambda x: 1 + for elm in self.document(all, {'rel': re.compile(r'\btag\b')}): + href = elm.get('href') + if not href: + continue + urlscheme, domain, path, params, query, fragment = \ + urlparse.urlparse(_urljoin(self.baseuri, href)) + segments = path.split('/') + tag = segments.pop() + if not tag: + if segments: + tag = segments.pop() + else: + # there are no tags + continue + tagscheme = urlparse.urlunparse((urlscheme, domain, '/'.join(segments), '', '', '')) + if not tagscheme.endswith('/'): + tagscheme += '/' + self.tags.append(FeedParserDict({"term": tag, "scheme": tagscheme, "label": elm.string or ''})) + + def findEnclosures(self): + all = lambda x: 1 + enclosure_match = re.compile(r'\benclosure\b') + for elm in self.document(all, {'href': re.compile(r'.+')}): + if not enclosure_match.search(elm.get('rel', u'')) and not self.isProbablyDownloadable(elm): + continue + if elm.attrMap not in self.enclosures: + self.enclosures.append(elm.attrMap) + if elm.string and not elm.get('title'): + self.enclosures[-1]['title'] = elm.string + + def findXFN(self): + all = lambda x: 1 + for elm in self.document(all, {'rel': re.compile('.+'), 'href': re.compile('.+')}): + rels = elm.get('rel', u'').split() + xfn_rels = [] + for rel in rels: + if rel in self.known_xfn_relationships: + xfn_rels.append(rel) + if xfn_rels: + self.xfn.append({"relationships": xfn_rels, "href": elm.get('href', ''), "name": elm.string}) + +def _parseMicroformats(htmlSource, baseURI, encoding): + if not BeautifulSoup: + return + try: + p = _MicroformatsParser(htmlSource, baseURI, encoding) + except UnicodeEncodeError: + # sgmllib throws this exception when performing lookups of tags + # with non-ASCII characters in them. + return + p.vcard = p.findVCards(p.document) + p.findTags() + p.findEnclosures() + p.findXFN() + return {"tags": p.tags, "enclosures": p.enclosures, "xfn": p.xfn, "vcard": p.vcard} class _RelativeURIResolver(_BaseHTMLProcessor): - - relative_uris = [ - ('a', 'href'), - ('applet', 'codebase'), - ('area', 'href'), - ('blockquote', 'cite'), - ('body', 'background'), - ('del', 'cite'), - ('form', 'action'), - ('frame', 'longdesc'), - ('frame', 'src'), - ('iframe', 'longdesc'), - ('iframe', 'src'), - ('head', 'profile'), - ('img', 'longdesc'), - ('img', 'src'), - ('img', 'usemap'), - ('input', 'src'), - ('input', 'usemap'), - ('ins', 'cite'), - ('link', 'href'), - ('object', 'classid'), - ('object', 'codebase'), - ('object', 'data'), - ('object', 'usemap'), - ('q', 'cite'), - ('script', 'src'), - ] - - def __init__(self, baseuri, encoding): - _BaseHTMLProcessor.__init__(self, encoding) + relative_uris = [('a', 'href'), + ('applet', 'codebase'), + ('area', 'href'), + ('blockquote', 'cite'), + ('body', 'background'), + ('del', 'cite'), + ('form', 'action'), + ('frame', 'longdesc'), + ('frame', 'src'), + ('iframe', 'longdesc'), + ('iframe', 'src'), + ('head', 'profile'), + ('img', 'longdesc'), + ('img', 'src'), + ('img', 'usemap'), + ('input', 'src'), + ('input', 'usemap'), + ('ins', 'cite'), + ('link', 'href'), + ('object', 'classid'), + ('object', 'codebase'), + ('object', 'data'), + ('object', 'usemap'), + ('q', 'cite'), + ('script', 'src')] + + def __init__(self, baseuri, encoding, _type): + _BaseHTMLProcessor.__init__(self, encoding, _type) self.baseuri = baseuri def resolveURI(self, uri): - return _urljoin(self.baseuri, uri) + return _makeSafeAbsoluteURI(_urljoin(self.baseuri, uri.strip())) def unknown_starttag(self, tag, attrs): attrs = self.normalize_attrs(attrs) - attrs = [(key, (tag, key) in self.relative_uris - and self.resolveURI(value) or value) for (key, - value) in attrs] + attrs = [(key, ((tag, key) in self.relative_uris) and self.resolveURI(value) or value) for key, value in attrs] _BaseHTMLProcessor.unknown_starttag(self, tag, attrs) +def _resolveRelativeURIs(htmlSource, baseURI, encoding, _type): + if not _SGML_AVAILABLE: + return htmlSource -def _resolveRelativeURIs(htmlSource, baseURI, encoding): - if _debug: - sys.stderr.write('entering _resolveRelativeURIs\n') - p = _RelativeURIResolver(baseURI, encoding) + p = _RelativeURIResolver(baseURI, encoding, _type) p.feed(htmlSource) return p.output() +def _makeSafeAbsoluteURI(base, rel=None): + # bail if ACCEPTABLE_URI_SCHEMES is empty + if not ACCEPTABLE_URI_SCHEMES: + return _urljoin(base, rel or u'') + if not base: + return rel or u'' + if not rel: + scheme = urlparse.urlparse(base)[0] + if not scheme or scheme in ACCEPTABLE_URI_SCHEMES: + return base + return u'' + uri = _urljoin(base, rel) + if uri.strip().split(':', 1)[0] not in ACCEPTABLE_URI_SCHEMES: + return u'' + return uri class _HTMLSanitizer(_BaseHTMLProcessor): - - acceptable_elements = [ - 'a', - 'abbr', - 'acronym', - 'address', - 'area', - 'b', - 'big', - 'blockquote', - 'br', - 'button', - 'caption', - 'center', - 'cite', - 'code', - 'col', - 'colgroup', - 'dd', - 'del', - 'dfn', - 'dir', - 'div', - 'dl', - 'dt', - 'em', - 'fieldset', - 'font', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'img', - 'input', - 'ins', - 'kbd', - 'label', - 'legend', - 'li', - 'map', - 'menu', - 'ol', - 'optgroup', - 'option', - 'p', - 'pre', - 'q', - 's', - 'samp', - 'select', - 'small', - 'span', - 'strike', - 'strong', - 'sub', - 'sup', - 'table', - 'tbody', - 'td', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'tr', - 'tt', - 'u', - 'ul', - 'var', - ] - - acceptable_attributes = [ - 'abbr', - 'accept', - 'accept-charset', - 'accesskey', - 'action', - 'align', - 'alt', - 'axis', - 'border', - 'cellpadding', - 'cellspacing', - 'char', - 'charoff', - 'charset', - 'checked', - 'cite', - 'class', - 'clear', - 'cols', - 'colspan', - 'color', - 'compact', - 'coords', - 'datetime', - 'dir', - 'disabled', - 'enctype', - 'for', - 'frame', - 'headers', - 'height', - 'href', - 'hreflang', - 'hspace', - 'id', - 'ismap', - 'label', - 'lang', - 'longdesc', - 'maxlength', - 'media', - 'method', - 'multiple', - 'name', - 'nohref', - 'noshade', - 'nowrap', - 'prompt', - 'readonly', - 'rel', - 'rev', - 'rows', - 'rowspan', - 'rules', - 'scope', - 'selected', - 'shape', - 'size', - 'span', - 'src', - 'start', - 'summary', - 'tabindex', - 'target', - 'title', - 'type', - 'usemap', - 'valign', - 'value', - 'vspace', - 'width', - ] - - unacceptable_elements_with_end_tag = ['script', 'applet'] + acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video', 'noscript'] + + acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', + 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', + 'background', 'balance', 'bgcolor', 'bgproperties', 'border', + 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', + 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', + 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', 'cols', + 'colspan', 'compact', 'contenteditable', 'controls', 'coords', 'data', + 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', 'delay', + 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', 'face', 'for', + 'form', 'frame', 'galleryimg', 'gutter', 'headers', 'height', 'hidefocus', + 'hidden', 'high', 'href', 'hreflang', 'hspace', 'icon', 'id', 'inputmode', + 'ismap', 'keytype', 'label', 'leftspacing', 'lang', 'list', 'longdesc', + 'loop', 'loopcount', 'loopend', 'loopstart', 'low', 'lowsrc', 'max', + 'maxlength', 'media', 'method', 'min', 'multiple', 'name', 'nohref', + 'noshade', 'nowrap', 'open', 'optimum', 'pattern', 'ping', 'point-size', + 'prompt', 'pqg', 'radiogroup', 'readonly', 'rel', 'repeat-max', + 'repeat-min', 'replace', 'required', 'rev', 'rightspacing', 'rows', + 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', + 'start', 'step', 'summary', 'suppress', 'tabindex', 'target', 'template', + 'title', 'toppadding', 'type', 'unselectable', 'usemap', 'urn', 'valign', + 'value', 'variable', 'volume', 'vspace', 'vrml', 'width', 'wrap', + 'xml:lang'] + + unacceptable_elements_with_end_tag = ['script', 'applet', 'style'] + + acceptable_css_properties = ['azimuth', 'background-color', + 'border-bottom-color', 'border-collapse', 'border-color', + 'border-left-color', 'border-right-color', 'border-top-color', 'clear', + 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', + 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', + 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', + 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', + 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', + 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', + 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', + 'white-space', 'width'] + + # survey of common keywords found in feeds + acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue', + 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', + 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', + 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', + 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', + 'transparent', 'underline', 'white', 'yellow'] + + valid_css_values = re.compile('^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|' + + '\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$') + + mathml_elements = ['annotation', 'annotation-xml', 'maction', 'math', + 'merror', 'mfenced', 'mfrac', 'mi', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', + 'mphantom', 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', + 'msub', 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', + 'munderover', 'none', 'semantics'] + + mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', + 'columnalign', 'close', 'columnlines', 'columnspacing', 'columnspan', 'depth', + 'display', 'displaystyle', 'encoding', 'equalcolumns', 'equalrows', + 'fence', 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', + 'lspace', 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', + 'maxsize', 'minsize', 'open', 'other', 'rowalign', 'rowalign', 'rowalign', + 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', + 'separator', 'separators', 'stretchy', 'width', 'width', 'xlink:href', + 'xlink:show', 'xlink:type', 'xmlns', 'xmlns:xlink'] + + # svgtiny - foreignObject + linearGradient + radialGradient + stop + svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', + 'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'foreignObject', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', 'mpath', + 'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop', + 'svg', 'switch', 'text', 'title', 'tspan', 'use'] + + # svgtiny + class + opacity + offset + xmlns + xmlns:xlink + svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', + 'arabic-form', 'ascent', 'attributeName', 'attributeType', + 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', + 'class', 'color', 'color-rendering', 'content', 'cx', 'cy', 'd', 'dx', + 'dy', 'descent', 'display', 'dur', 'end', 'fill', 'fill-opacity', + 'fill-rule', 'font-family', 'font-size', 'font-stretch', 'font-style', + 'font-variant', 'font-weight', 'from', 'fx', 'fy', 'g1', 'g2', + 'glyph-name', 'gradientUnits', 'hanging', 'height', 'horiz-adv-x', + 'horiz-origin-x', 'id', 'ideographic', 'k', 'keyPoints', 'keySplines', + 'keyTimes', 'lang', 'mathematical', 'marker-end', 'marker-mid', + 'marker-start', 'markerHeight', 'markerUnits', 'markerWidth', 'max', + 'min', 'name', 'offset', 'opacity', 'orient', 'origin', + 'overline-position', 'overline-thickness', 'panose-1', 'path', + 'pathLength', 'points', 'preserveAspectRatio', 'r', 'refX', 'refY', + 'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures', + 'restart', 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', + 'stop-color', 'stop-opacity', 'strikethrough-position', + 'strikethrough-thickness', 'stroke', 'stroke-dasharray', + 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', + 'target', 'text-anchor', 'to', 'transform', 'type', 'u1', 'u2', + 'underline-position', 'underline-thickness', 'unicode', 'unicode-range', + 'units-per-em', 'values', 'version', 'viewBox', 'visibility', 'width', + 'widths', 'x', 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', + 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', + 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', 'y1', + 'y2', 'zoomAndPan'] + + svg_attr_map = None + svg_elem_map = None + + acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule', + 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', + 'stroke-opacity'] def reset(self): _BaseHTMLProcessor.reset(self) self.unacceptablestack = 0 + self.mathmlOK = 0 + self.svgOK = 0 def unknown_starttag(self, tag, attrs): - if not tag in self.acceptable_elements: + acceptable_attributes = self.acceptable_attributes + keymap = {} + if not tag in self.acceptable_elements or self.svgOK: if tag in self.unacceptable_elements_with_end_tag: self.unacceptablestack += 1 - return - attrs = self.normalize_attrs(attrs) - attrs = [(key, value) for (key, value) in attrs if key - in self.acceptable_attributes] - _BaseHTMLProcessor.unknown_starttag(self, tag, attrs) + + # add implicit namespaces to html5 inline svg/mathml + if self._type.endswith('html'): + if not dict(attrs).get('xmlns'): + if tag=='svg': + attrs.append( ('xmlns','http://www.w3.org/2000/svg') ) + if tag=='math': + attrs.append( ('xmlns','http://www.w3.org/1998/Math/MathML') ) + + # not otherwise acceptable, perhaps it is MathML or SVG? + if tag=='math' and ('xmlns','http://www.w3.org/1998/Math/MathML') in attrs: + self.mathmlOK += 1 + if tag=='svg' and ('xmlns','http://www.w3.org/2000/svg') in attrs: + self.svgOK += 1 + + # chose acceptable attributes based on tag class, else bail + if self.mathmlOK and tag in self.mathml_elements: + acceptable_attributes = self.mathml_attributes + elif self.svgOK and tag in self.svg_elements: + # for most vocabularies, lowercasing is a good idea. Many + # svg elements, however, are camel case + if not self.svg_attr_map: + lower=[attr.lower() for attr in self.svg_attributes] + mix=[a for a in self.svg_attributes if a not in lower] + self.svg_attributes = lower + self.svg_attr_map = dict([(a.lower(),a) for a in mix]) + + lower=[attr.lower() for attr in self.svg_elements] + mix=[a for a in self.svg_elements if a not in lower] + self.svg_elements = lower + self.svg_elem_map = dict([(a.lower(),a) for a in mix]) + acceptable_attributes = self.svg_attributes + tag = self.svg_elem_map.get(tag,tag) + keymap = self.svg_attr_map + elif not tag in self.acceptable_elements: + return + + # declare xlink namespace, if needed + if self.mathmlOK or self.svgOK: + if filter(lambda (n,v): n.startswith('xlink:'),attrs): + if not ('xmlns:xlink','http://www.w3.org/1999/xlink') in attrs: + attrs.append(('xmlns:xlink','http://www.w3.org/1999/xlink')) + + clean_attrs = [] + for key, value in self.normalize_attrs(attrs): + if key in acceptable_attributes: + key=keymap.get(key,key) + # make sure the uri uses an acceptable uri scheme + if key == u'href': + value = _makeSafeAbsoluteURI(value) + clean_attrs.append((key,value)) + elif key=='style': + clean_value = self.sanitize_style(value) + if clean_value: + clean_attrs.append((key,clean_value)) + _BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs) def unknown_endtag(self, tag): if not tag in self.acceptable_elements: if tag in self.unacceptable_elements_with_end_tag: self.unacceptablestack -= 1 - return + if self.mathmlOK and tag in self.mathml_elements: + if tag == 'math' and self.mathmlOK: + self.mathmlOK -= 1 + elif self.svgOK and tag in self.svg_elements: + tag = self.svg_elem_map.get(tag,tag) + if tag == 'svg' and self.svgOK: + self.svgOK -= 1 + else: + return _BaseHTMLProcessor.unknown_endtag(self, tag) def handle_pi(self, text): pass @@ -2420,47 +2759,83 @@ def handle_data(self, text): if not self.unacceptablestack: _BaseHTMLProcessor.handle_data(self, text) + def sanitize_style(self, style): + # disallow urls + style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style) -def _sanitizeHTML(htmlSource, encoding): - p = _HTMLSanitizer(encoding) + # gauntlet + if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): + return '' + # This replaced a regexp that used re.match and was prone to pathological back-tracking. + if re.sub("\s*[-\w]+\s*:\s*[^:;]*;?", '', style).strip(): + return '' + + clean = [] + for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style): + if not value: + continue + if prop.lower() in self.acceptable_css_properties: + clean.append(prop + ': ' + value + ';') + elif prop.split('-')[0].lower() in ['background','border','margin','padding']: + for keyword in value.split(): + if not keyword in self.acceptable_css_keywords and \ + not self.valid_css_values.match(keyword): + break + else: + clean.append(prop + ': ' + value + ';') + elif self.svgOK and prop.lower() in self.acceptable_svg_properties: + clean.append(prop + ': ' + value + ';') + + return ' '.join(clean) + + def parse_comment(self, i, report=1): + ret = _BaseHTMLProcessor.parse_comment(self, i, report) + if ret >= 0: + return ret + # if ret == -1, this may be a malicious attempt to circumvent + # sanitization, or a page-destroying unclosed comment + match = re.compile(r'--[^>]*>').search(self.rawdata, i+4) + if match: + return match.end() + # unclosed comment; deliberately fail to handle_data() + return len(self.rawdata) + + +def _sanitizeHTML(htmlSource, encoding, _type): + if not _SGML_AVAILABLE: + return htmlSource + p = _HTMLSanitizer(encoding, _type) + htmlSource = htmlSource.replace(''): @@ -2468,124 +2843,54 @@ if data.count('= '2.3.3' - assert base64 != None - (user, passw) = \ - base64.decodestring(req.headers['Authorization' - ].split(' ')[1]).split(':') - realm = re.findall('realm="([^"]*)"', - headers['WWW-Authenticate'])[0] - self.add_password(realm, host, user, passw) - retry = self.http_error_auth_reqed('www-authenticate', - host, req, headers) - self.reset_retry_count() - return retry - except: + if base64 is None or 'Authorization' not in req.headers \ + or 'WWW-Authenticate' not in headers: return self.http_error_default(req, fp, code, msg, headers) - - -def _open_resource( - url_file_stream_or_string, - etag, - modified, - agent, - referrer, - handlers, - ): + auth = _base64decode(req.headers['Authorization'].split(' ')[1]) + user, passw = auth.split(':') + realm = re.findall('realm="([^"]*)"', headers['WWW-Authenticate'])[0] + self.add_password(realm, host, user, passw) + retry = self.http_error_auth_reqed('www-authenticate', host, req, headers) + self.reset_retry_count() + return retry + +def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers, request_headers): """URL, filename, or string --> stream This function lets you define parsers that take any input source (URL, pathname to local or network file, or actual data as a string) and deal with it in a uniform manner. Returned object is guaranteed @@ -2593,139 +2898,146 @@ Just .close() the object when you're done with it. If the etag argument is supplied, it will be used as the value of an If-None-Match request header. - If the modified argument is supplied, it must be a tuple of 9 integers - as returned by gmtime() in the standard Python time module. This MUST - be in GMT (Greenwich Mean Time). The formatted date/time will be used - as the value of an If-Modified-Since request header. + If the modified argument is supplied, it can be a tuple of 9 integers + (as returned by gmtime() in the standard Python time module) or a date + string in any format supported by feedparser. Regardless, it MUST + be in GMT (Greenwich Mean Time). It will be reformatted into an + RFC 1123-compliant date and used as the value of an If-Modified-Since + request header. If the agent argument is supplied, it will be used as the value of a User-Agent request header. If the referrer argument is supplied, it will be used as the value of a Referer[sic] request header. If handlers is supplied, it is a list of handlers used to build a urllib2 opener. + + if request_headers is supplied it is a dictionary of HTTP request headers + that will override the values generated by FeedParser. """ if hasattr(url_file_stream_or_string, 'read'): return url_file_stream_or_string if url_file_stream_or_string == '-': return sys.stdin - if urlparse.urlparse(url_file_stream_or_string)[0] in ('http', - 'https', 'ftp'): + if isinstance(url_file_stream_or_string, basestring) \ + and urlparse.urlparse(url_file_stream_or_string)[0] in ('http', 'https', 'ftp', 'file', 'feed'): + # Deal with the feed URI scheme + if url_file_stream_or_string.startswith('feed:http'): + url_file_stream_or_string = url_file_stream_or_string[5:] + elif url_file_stream_or_string.startswith('feed:'): + url_file_stream_or_string = 'http:' + url_file_stream_or_string[5:] if not agent: agent = USER_AGENT - # test for inline user:password for basic auth - auth = None if base64: - (urltype, rest) = \ - urllib.splittype(url_file_stream_or_string) - (realhost, rest) = urllib.splithost(rest) + urltype, rest = urllib.splittype(url_file_stream_or_string) + realhost, rest = urllib.splithost(rest) if realhost: - (user_passwd, realhost) = urllib.splituser(realhost) + user_passwd, realhost = urllib.splituser(realhost) if user_passwd: - url_file_stream_or_string = '%s://%s%s' % (urltype, - realhost, rest) - auth = base64.encodestring(user_passwd).strip() + url_file_stream_or_string = '%s://%s%s' % (urltype, realhost, rest) + auth = base64.standard_b64encode(user_passwd).strip() + + # iri support + if isinstance(url_file_stream_or_string, unicode): + url_file_stream_or_string = _convert_to_idn(url_file_stream_or_string) # try to open with urllib2 (to use optional headers) - - request = urllib2.Request(url_file_stream_or_string) - request.add_header('User-Agent', agent) - if etag: - request.add_header('If-None-Match', etag) - if modified: - - # format into an RFC 1123-compliant timestamp. We can't use - # time.strftime() since the %a and %b directives can be affected - # by the current locale, but RFC 2616 states that dates must be - # in English. - - short_weekdays = [ - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat', - 'Sun', - ] - months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ] - request.add_header('If-Modified-Since', - '%s, %02d %s %04d %02d:%02d:%02d GMT' % ( - short_weekdays[modified[6]], - modified[2], - months[modified[1] - 1], - modified[0], - modified[3], - modified[4], - modified[5], - )) - if referrer: - request.add_header('Referer', referrer) - if gzip and zlib: - request.add_header('Accept-encoding', 'gzip, deflate') - elif gzip: - request.add_header('Accept-encoding', 'gzip') - elif zlib: - request.add_header('Accept-encoding', 'deflate') - else: - request.add_header('Accept-encoding', '') - if auth: - request.add_header('Authorization', 'Basic %s' % auth) - if ACCEPT_HEADER: - request.add_header('Accept', ACCEPT_HEADER) - request.add_header('A-IM', 'feed') # RFC 3229 support - opener = apply(urllib2.build_opener, tuple([_FeedURLHandler()] - + handlers)) - opener.addheaders = [] # RMK - must clear so we only send our custom User-Agent + request = _build_urllib2_request(url_file_stream_or_string, agent, etag, modified, referrer, auth, request_headers) + opener = apply(urllib2.build_opener, tuple(handlers + [_FeedURLHandler()])) + opener.addheaders = [] # RMK - must clear so we only send our custom User-Agent try: return opener.open(request) finally: - opener.close() # JohnD + opener.close() # JohnD # try to open with native open function (if url_file_stream_or_string is a filename) - try: - return open(url_file_stream_or_string) - except: + return open(url_file_stream_or_string, 'rb') + except IOError: pass # treat url_file_stream_or_string as string + if isinstance(url_file_stream_or_string, unicode): + return _StringIO(url_file_stream_or_string.encode('utf-8')) + return _StringIO(url_file_stream_or_string) - return _StringIO(str(url_file_stream_or_string)) +def _convert_to_idn(url): + """Convert a URL to IDN notation""" + # this function should only be called with a unicode string + # strategy: if the host cannot be encoded in ascii, then + # it'll be necessary to encode it in idn form + parts = list(urlparse.urlsplit(url)) + try: + parts[1].encode('ascii') + except UnicodeEncodeError: + # the url needs to be converted to idn notation + host = parts[1].rsplit(':', 1) + newhost = [] + port = u'' + if len(host) == 2: + port = host.pop() + for h in host[0].split('.'): + newhost.append(h.encode('idna').decode('utf-8')) + parts[1] = '.'.join(newhost) + if port: + parts[1] += ':' + port + return urlparse.urlunsplit(parts) + else: + return url +def _build_urllib2_request(url, agent, etag, modified, referrer, auth, request_headers): + request = urllib2.Request(url) + request.add_header('User-Agent', agent) + if etag: + request.add_header('If-None-Match', etag) + if isinstance(modified, basestring): + modified = _parse_date(modified) + elif isinstance(modified, datetime.datetime): + modified = modified.utctimetuple() + if modified: + # format into an RFC 1123-compliant timestamp. We can't use + # time.strftime() since the %a and %b directives can be affected + # by the current locale, but RFC 2616 states that dates must be + # in English. + short_weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + request.add_header('If-Modified-Since', '%s, %02d %s %04d %02d:%02d:%02d GMT' % (short_weekdays[modified[6]], modified[2], months[modified[1] - 1], modified[0], modified[3], modified[4], modified[5])) + if referrer: + request.add_header('Referer', referrer) + if gzip and zlib: + request.add_header('Accept-encoding', 'gzip, deflate') + elif gzip: + request.add_header('Accept-encoding', 'gzip') + elif zlib: + request.add_header('Accept-encoding', 'deflate') + else: + request.add_header('Accept-encoding', '') + if auth: + request.add_header('Authorization', 'Basic %s' % auth) + if ACCEPT_HEADER: + request.add_header('Accept', ACCEPT_HEADER) + # use this for whatever -- cookies, special headers, etc + # [('Cookie','Something'),('x-special-header','Another Value')] + for header_name, header_value in request_headers.items(): + request.add_header(header_name, header_value) + request.add_header('A-IM', 'feed') # RFC 3229 support + return request _date_handlers = [] - - def registerDateHandler(func): - """Register a date handler function (takes string, returns 9-tuple date in GMT)""" - + '''Register a date handler function (takes string, returns 9-tuple date in GMT)''' _date_handlers.insert(0, func) - # ISO-8601 date parsing routines written by Fazal Majid. # The ISO 8601 standard is very convoluted and irregular - a full ISO 8601 # parser is beyond the scope of feedparser and would be a worthwhile addition # to the Python library. @@ -2732,44 +3044,40 @@ # A single regular expression cannot parse ISO 8601 date formats into groups # as the standard is highly irregular (for instance is 030104 2003-01-04 or # 0301-04-01), so we use templates instead. # Please note the order in templates is significant because we need a # greedy match. - -_iso8601_tmpl = [ - 'YYYY-?MM-?DD', - 'YYYY-MM', - 'YYYY-?OOO', - 'YY-?MM-?DD', - 'YY-?OOO', - 'YYYY', - '-YY-?MM', - '-OOO', - '-YY', - '--MM-?DD', - '--MM', - '---DD', - 'CC', - '', - ] -_iso8601_re = [tmpl.replace('YYYY', r'(?P\d{4})').replace('YY', - r'(?P\d\d)').replace('MM', r'(?P[01]\d)' - ).replace('DD', r'(?P[0123]\d)').replace('OOO', - r'(?P[0123]\d\d)').replace('CC', - r'(?P\d\d$)') - + r'(T?(?P\d{2}):(?P\d{2})' - + r'(:(?P\d{2}))?' - + r'(?P[+-](?P\d{2})(:(?P\d{2}))?|Z)?)?' - for tmpl in _iso8601_tmpl] -del tmpl +_iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-0MM?-?DD', 'YYYY-MM', 'YYYY-?OOO', + 'YY-?MM-?DD', 'YY-?OOO', 'YYYY', + '-YY-?MM', '-OOO', '-YY', + '--MM-?DD', '--MM', + '---DD', + 'CC', ''] +_iso8601_re = [ + tmpl.replace( + 'YYYY', r'(?P\d{4})').replace( + 'YY', r'(?P\d\d)').replace( + 'MM', r'(?P[01]\d)').replace( + 'DD', r'(?P[0123]\d)').replace( + 'OOO', r'(?P[0123]\d\d)').replace( + 'CC', r'(?P\d\d$)') + + r'(T?(?P\d{2}):(?P\d{2})' + + r'(:(?P\d{2}))?' + + r'(\.(?P\d+))?' + + r'(?P[+-](?P\d{2})(:(?P\d{2}))?|Z)?)?' + for tmpl in _iso8601_tmpl] +try: + del tmpl +except NameError: + pass _iso8601_matches = [re.compile(regex).match for regex in _iso8601_re] -del regex - - +try: + del regex +except NameError: + pass def _parse_date_iso8601(dateString): - """Parse a variety of ISO-8601-compatible formats like 20040105""" - + '''Parse a variety of ISO-8601-compatible formats like 20040105''' m = None for _iso8601_match in _iso8601_matches: m = _iso8601_match(dateString) if m: break @@ -2785,81 +3093,52 @@ ordinal = 0 year = params.get('year', '--') if not year or year == '--': year = time.gmtime()[0] elif len(year) == 2: - # ISO 8601 assumes current century, i.e. 93 -> 2093, NOT 1993 - year = 100 * int(time.gmtime()[0] / 100) + int(year) else: year = int(year) month = params.get('month', '-') if not month or month == '-': - # ordinals are NOT normalized by mktime, we simulate them # by setting month=1, day=ordinal - if ordinal: month = 1 else: month = time.gmtime()[1] month = int(month) day = params.get('day', 0) if not day: - # see above - if ordinal: day = ordinal - elif params.get('century', 0) or params.get('year', 0)\ - or params.get('month', 0): + elif params.get('century', 0) or \ + params.get('year', 0) or params.get('month', 0): day = 1 else: day = time.gmtime()[2] else: day = int(day) - # special case of the century - is the first year of the 21st century # 2000 or 2001 ? The debate goes on... - if 'century' in params.keys(): year = (int(params['century']) - 1) * 100 + 1 - # in ISO 8601 most fields are optional - for field in ['hour', 'minute', 'second', 'tzhour', 'tzmin']: if not params.get(field, None): params[field] = 0 hour = int(params.get('hour', 0)) minute = int(params.get('minute', 0)) - second = int(params.get('second', 0)) - + second = int(float(params.get('second', 0))) # weekday is normalized by mktime(), we can ignore it - weekday = 0 - - # daylight savings is complex, but not needed for feedparser's purposes - # as time zones, if specified, include mention of whether it is active - # (e.g. PST vs. PDT, CET). Using -1 is implementation-dependent and - # and most implementations have DST bugs - - daylight_savings_flag = 0 - tm = [ - year, - month, - day, - hour, - minute, - second, - weekday, - ordinal, - daylight_savings_flag, - ] - + daylight_savings_flag = -1 + tm = [year, month, day, hour, minute, second, weekday, + ordinal, daylight_savings_flag] # ISO 8601 time zone adjustments - tz = params.get('tz') if tz and tz != 'Z': if tz[0] == '-': tm[3] += int(params.get('tzhour', 0)) tm[4] += int(params.get('tzmin', 0)) @@ -2866,276 +3145,184 @@ elif tz[0] == '+': tm[3] -= int(params.get('tzhour', 0)) tm[4] -= int(params.get('tzmin', 0)) else: return None - # Python's time.mktime() is a wrapper around the ANSI C mktime(3c) # which is guaranteed to normalize d/m/y/h/m/s. # Many implementations have bugs, but we'll pretend they don't. - - return time.localtime(time.mktime(tm)) - - + return time.localtime(time.mktime(tuple(tm))) registerDateHandler(_parse_date_iso8601) # 8-bit date handling routines written by ytrewq1. - -_korean_year = u'\ub144' # b3e2 in euc-kr -_korean_month = u'\uc6d4' # bff9 in euc-kr -_korean_day = u'\uc77c' # c0cf in euc-kr -_korean_am = u'\uc624\uc804' # bfc0 c0fc in euc-kr -_korean_pm = u'\uc624\ud6c4' # bfc0 c8c4 in euc-kr +_korean_year = u'\ub144' # b3e2 in euc-kr +_korean_month = u'\uc6d4' # bff9 in euc-kr +_korean_day = u'\uc77c' # c0cf in euc-kr +_korean_am = u'\uc624\uc804' # bfc0 c0fc in euc-kr +_korean_pm = u'\uc624\ud6c4' # bfc0 c8c4 in euc-kr _korean_onblog_date_re = \ - re.compile('(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})' - % (_korean_year, _korean_month, _korean_day)) + re.compile('(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})' % \ + (_korean_year, _korean_month, _korean_day)) _korean_nate_date_re = \ - re.compile(u'(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})' - % (_korean_am, _korean_pm)) - - + re.compile(u'(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})' % \ + (_korean_am, _korean_pm)) def _parse_date_onblog(dateString): - """Parse a string according to the OnBlog 8-bit date format""" - + '''Parse a string according to the OnBlog 8-bit date format''' m = _korean_onblog_date_re.match(dateString) if not m: return - w3dtfdate = \ - '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s'\ - % { - 'year': m.group(1), - 'month': m.group(2), - 'day': m.group(3), - 'hour': m.group(4), - 'minute': m.group(5), - 'second': m.group(6), - 'zonediff': '+09:00', - } - if _debug: - sys.stderr.write('OnBlog date parsed as: %s\n' % w3dtfdate) + w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \ + {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\ + 'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\ + 'zonediff': '+09:00'} return _parse_date_w3dtf(w3dtfdate) - - registerDateHandler(_parse_date_onblog) - def _parse_date_nate(dateString): - """Parse a string according to the Nate 8-bit date format""" - + '''Parse a string according to the Nate 8-bit date format''' m = _korean_nate_date_re.match(dateString) if not m: return hour = int(m.group(5)) ampm = m.group(4) - if ampm == _korean_pm: + if (ampm == _korean_pm): hour += 12 hour = str(hour) if len(hour) == 1: hour = '0' + hour - w3dtfdate = \ - '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s'\ - % { - 'year': m.group(1), - 'month': m.group(2), - 'day': m.group(3), - 'hour': hour, - 'minute': m.group(6), - 'second': m.group(7), - 'zonediff': '+09:00', - } - if _debug: - sys.stderr.write('Nate date parsed as: %s\n' % w3dtfdate) + w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \ + {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\ + 'hour': hour, 'minute': m.group(6), 'second': m.group(7),\ + 'zonediff': '+09:00'} return _parse_date_w3dtf(w3dtfdate) - - registerDateHandler(_parse_date_nate) _mssql_date_re = \ - re.compile('(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(\.\d+)?' - ) - - + re.compile('(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})(\.\d+)?') def _parse_date_mssql(dateString): - """Parse a string according to the MS SQL date format""" - + '''Parse a string according to the MS SQL date format''' m = _mssql_date_re.match(dateString) if not m: return - w3dtfdate = \ - '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s'\ - % { - 'year': m.group(1), - 'month': m.group(2), - 'day': m.group(3), - 'hour': m.group(4), - 'minute': m.group(5), - 'second': m.group(6), - 'zonediff': '+09:00', - } - if _debug: - sys.stderr.write('MS SQL date parsed as: %s\n' % w3dtfdate) + w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s' % \ + {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\ + 'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\ + 'zonediff': '+09:00'} return _parse_date_w3dtf(w3dtfdate) - - registerDateHandler(_parse_date_mssql) # Unicode strings for Greek date strings - -_greek_months = { - u'\u0399\u03b1\u03bd': u'Jan', - u'\u03a6\u03b5\u03b2': u'Feb', - u'\u039c\u03ac\u03ce': u'Mar', - u'\u039c\u03b1\u03ce': u'Mar', - u'\u0391\u03c0\u03c1': u'Apr', - u'\u039c\u03ac\u03b9': u'May', - u'\u039c\u03b1\u03ca': u'May', - u'\u039c\u03b1\u03b9': u'May', - u'\u0399\u03bf\u03cd\u03bd': u'Jun', - u'\u0399\u03bf\u03bd': u'Jun', - u'\u0399\u03bf\u03cd\u03bb': u'Jul', - u'\u0399\u03bf\u03bb': u'Jul', - u'\u0391\u03cd\u03b3': u'Aug', - u'\u0391\u03c5\u03b3': u'Aug', - u'\u03a3\u03b5\u03c0': u'Sep', - u'\u039f\u03ba\u03c4': u'Oct', - u'\u039d\u03bf\u03ad': u'Nov', - u'\u039d\u03bf\u03b5': u'Nov', - u'\u0394\u03b5\u03ba': u'Dec', - } - -_greek_wdays = { - u'\u039a\u03c5\u03c1': u'Sun', - u'\u0394\u03b5\u03c5': u'Mon', - u'\u03a4\u03c1\u03b9': u'Tue', - u'\u03a4\u03b5\u03c4': u'Wed', - u'\u03a0\u03b5\u03bc': u'Thu', - u'\u03a0\u03b1\u03c1': u'Fri', - u'\u03a3\u03b1\u03b2': u'Sat', - } +_greek_months = \ + { \ + u'\u0399\u03b1\u03bd': u'Jan', # c9e1ed in iso-8859-7 + u'\u03a6\u03b5\u03b2': u'Feb', # d6e5e2 in iso-8859-7 + u'\u039c\u03ac\u03ce': u'Mar', # ccdcfe in iso-8859-7 + u'\u039c\u03b1\u03ce': u'Mar', # cce1fe in iso-8859-7 + u'\u0391\u03c0\u03c1': u'Apr', # c1f0f1 in iso-8859-7 + u'\u039c\u03ac\u03b9': u'May', # ccdce9 in iso-8859-7 + u'\u039c\u03b1\u03ca': u'May', # cce1fa in iso-8859-7 + u'\u039c\u03b1\u03b9': u'May', # cce1e9 in iso-8859-7 + u'\u0399\u03bf\u03cd\u03bd': u'Jun', # c9effded in iso-8859-7 + u'\u0399\u03bf\u03bd': u'Jun', # c9efed in iso-8859-7 + u'\u0399\u03bf\u03cd\u03bb': u'Jul', # c9effdeb in iso-8859-7 + u'\u0399\u03bf\u03bb': u'Jul', # c9f9eb in iso-8859-7 + u'\u0391\u03cd\u03b3': u'Aug', # c1fde3 in iso-8859-7 + u'\u0391\u03c5\u03b3': u'Aug', # c1f5e3 in iso-8859-7 + u'\u03a3\u03b5\u03c0': u'Sep', # d3e5f0 in iso-8859-7 + u'\u039f\u03ba\u03c4': u'Oct', # cfeaf4 in iso-8859-7 + u'\u039d\u03bf\u03ad': u'Nov', # cdefdd in iso-8859-7 + u'\u039d\u03bf\u03b5': u'Nov', # cdefe5 in iso-8859-7 + u'\u0394\u03b5\u03ba': u'Dec', # c4e5ea in iso-8859-7 + } + +_greek_wdays = \ + { \ + u'\u039a\u03c5\u03c1': u'Sun', # caf5f1 in iso-8859-7 + u'\u0394\u03b5\u03c5': u'Mon', # c4e5f5 in iso-8859-7 + u'\u03a4\u03c1\u03b9': u'Tue', # d4f1e9 in iso-8859-7 + u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7 + u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7 + u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7 + u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7 + } _greek_date_format_re = \ - re.compile(u'([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)' - ) - + re.compile(u'([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)') def _parse_date_greek(dateString): - """Parse a string according to a Greek 8-bit date format.""" - + '''Parse a string according to a Greek 8-bit date format.''' m = _greek_date_format_re.match(dateString) if not m: return - try: - wday = _greek_wdays[m.group(1)] - month = _greek_months[m.group(3)] - except: - return - rfc822date = \ - '%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(zonediff)s'\ - % { - 'wday': wday, - 'day': m.group(2), - 'month': month, - 'year': m.group(4), - 'hour': m.group(5), - 'minute': m.group(6), - 'second': m.group(7), - 'zonediff': m.group(8), - } - if _debug: - sys.stderr.write('Greek date parsed as: %s\n' % rfc822date) + wday = _greek_wdays[m.group(1)] + month = _greek_months[m.group(3)] + rfc822date = '%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(zonediff)s' % \ + {'wday': wday, 'day': m.group(2), 'month': month, 'year': m.group(4),\ + 'hour': m.group(5), 'minute': m.group(6), 'second': m.group(7),\ + 'zonediff': m.group(8)} return _parse_date_rfc822(rfc822date) - - registerDateHandler(_parse_date_greek) # Unicode strings for Hungarian date strings - -_hungarian_months = { - u'janu\u00e1r': u'01', - u'febru\u00e1ri': u'02', - u'm\u00e1rcius': u'03', - u'\u00e1prilis': u'04', - u'm\u00e1ujus': u'05', - u'j\u00fanius': u'06', - u'j\u00falius': u'07', - u'augusztus': u'08', - u'szeptember': u'09', - u'okt\u00f3ber': u'10', - u'november': u'11', - u'december': u'12', - } +_hungarian_months = \ + { \ + u'janu\u00e1r': u'01', # e1 in iso-8859-2 + u'febru\u00e1ri': u'02', # e1 in iso-8859-2 + u'm\u00e1rcius': u'03', # e1 in iso-8859-2 + u'\u00e1prilis': u'04', # e1 in iso-8859-2 + u'm\u00e1ujus': u'05', # e1 in iso-8859-2 + u'j\u00fanius': u'06', # fa in iso-8859-2 + u'j\u00falius': u'07', # fa in iso-8859-2 + u'augusztus': u'08', + u'szeptember': u'09', + u'okt\u00f3ber': u'10', # f3 in iso-8859-2 + u'november': u'11', + u'december': u'12', + } _hungarian_date_format_re = \ - re.compile(u'(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})((\+|-)(\d{,2}:\d{2}))' - ) - + re.compile(u'(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})((\+|-)(\d{,2}:\d{2}))') def _parse_date_hungarian(dateString): - """Parse a string according to a Hungarian 8-bit date format.""" - + '''Parse a string according to a Hungarian 8-bit date format.''' m = _hungarian_date_format_re.match(dateString) - if not m: - return - try: - month = _hungarian_months[m.group(2)] - day = m.group(3) - if len(day) == 1: - day = '0' + day - hour = m.group(4) - if len(hour) == 1: - hour = '0' + hour - except: - return - w3dtfdate = \ - '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s%(zonediff)s' % { - 'year': m.group(1), - 'month': month, - 'day': day, - 'hour': hour, - 'minute': m.group(5), - 'zonediff': m.group(6), - } - if _debug: - sys.stderr.write('Hungarian date parsed as: %s\n' % w3dtfdate) + if not m or m.group(2) not in _hungarian_months: + return None + month = _hungarian_months[m.group(2)] + day = m.group(3) + if len(day) == 1: + day = '0' + day + hour = m.group(4) + if len(hour) == 1: + hour = '0' + hour + w3dtfdate = '%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s%(zonediff)s' % \ + {'year': m.group(1), 'month': month, 'day': day,\ + 'hour': hour, 'minute': m.group(5),\ + 'zonediff': m.group(6)} return _parse_date_w3dtf(w3dtfdate) - - registerDateHandler(_parse_date_hungarian) # W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by # Drake and licensed under the Python license. Removed all range checking # for month, day, hour, minute, and second, since mktime will normalize # these later - - def _parse_date_w3dtf(dateString): - def __extract_date(m): year = int(m.group('year')) if year < 100: year = 100 * int(time.gmtime()[0] / 100) + int(year) if year < 1000: - return (0, 0, 0) + return 0, 0, 0 julian = m.group('julian') if julian: julian = int(julian) month = julian / 30 + 1 day = julian % 30 + 1 jday = None while jday != julian: - t = time.mktime(( - year, - month, - day, - 0, - 0, - 0, - 0, - 0, - 0, - )) + t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0)) jday = time.gmtime(t)[-2] diff = abs(jday - julian) if jday > julian: if diff < day: day = day - diff @@ -3142,14 +3329,14 @@ else: month = month - 1 day = 31 elif jday < julian: if day + diff < 28: - day = day + diff + day = day + diff else: month = month + 1 - return (year, month, day) + return year, month, day month = m.group('month') day = 1 if month is None: month = 1 else: @@ -3157,30 +3344,29 @@ day = m.group('day') if day: day = int(day) else: day = 1 - return (year, month, day) + return year, month, day def __extract_time(m): if not m: - return (0, 0, 0) + return 0, 0, 0 hours = m.group('hours') if not hours: - return (0, 0, 0) + return 0, 0, 0 hours = int(hours) minutes = int(m.group('minutes')) seconds = m.group('seconds') if seconds: seconds = int(seconds) else: seconds = 0 - return (hours, minutes, seconds) + return hours, minutes, seconds def __extract_tzd(m): - """Return the Time Zone Designator as an offset in seconds from UTC.""" - + '''Return the Time Zone Designator as an offset in seconds from UTC.''' if not m: return 0 tzd = m.group('tzd') if not tzd: return 0 @@ -3190,99 +3376,105 @@ minutes = m.group('tzdminutes') if minutes: minutes = int(minutes) else: minutes = 0 - offset = (hours * 60 + minutes) * 60 + offset = (hours*60 + minutes) * 60 if tzd[0] == '+': return -offset return offset - __date_re = \ - '(?P\\d\\d\\d\\d)(?:(?P-|)(?:(?P\\d\\d\\d)|(?P\\d\\d)(?:(?P=dsep)(?P\\d\\d))?))?' - - __tzd_re = \ - '(?P[-+](?P\d\d)(?::?(?P\d\d))|Z)' + __date_re = ('(?P\d\d\d\d)' + '(?:(?P-|)' + '(?:(?P\d\d)(?:(?P=dsep)(?P\d\d))?' + '|(?P\d\d\d)))?') + __tzd_re = '(?P[-+](?P\d\d)(?::?(?P\d\d))|Z)' __tzd_rx = re.compile(__tzd_re) - __time_re = \ - '(?P\\d\\d)(?P:|)(?P\\d\\d)(?:(?P=tsep)(?P\\d\\d(?:[.,]\\d+)?))?'\ - + __tzd_re + __time_re = ('(?P\d\d)(?P:|)(?P\d\d)' + '(?:(?P=tsep)(?P\d\d)(?:[.,]\d+)?)?' + + __tzd_re) __datetime_re = '%s(?:T%s)?' % (__date_re, __time_re) __datetime_rx = re.compile(__datetime_re) m = __datetime_rx.match(dateString) - if m is None or m.group() != dateString: + if (m is None) or (m.group() != dateString): return gmt = __extract_date(m) + __extract_time(m) + (0, 0, 0) if gmt[0] == 0: return - return time.gmtime((time.mktime(gmt) + __extract_tzd(m)) - - time.timezone) - - + return time.gmtime(time.mktime(gmt) + __extract_tzd(m) - time.timezone) registerDateHandler(_parse_date_w3dtf) - def _parse_date_rfc822(dateString): - """Parse an RFC822, RFC1123, RFC2822, or asctime-style date""" - + '''Parse an RFC822, RFC1123, RFC2822, or asctime-style date''' data = dateString.split() + if not data: + return None if data[0][-1] in (',', '.') or data[0].lower() in rfc822._daynames: del data[0] if len(data) == 4: s = data[3] i = s.find('+') if i > 0: - data[3:] = [s[:i], s[i + 1:]] + data[3:] = [s[:i], s[i+1:]] else: data.append('') - dateString = ' '.join(data) + dateString = " ".join(data) + # Account for the Etc/GMT timezone by stripping 'Etc/' + elif len(data) == 5 and data[4].lower().startswith('etc/'): + data[4] = data[4][4:] + dateString = " ".join(data) if len(data) < 5: dateString += ' 00:00:00 GMT' tm = rfc822.parsedate_tz(dateString) if tm: + # Jython doesn't adjust for 2-digit years like CPython does, + # so account for it by shifting the year so that it's in the + # range 1970-2069 (1970 being the year of the Unix epoch). + if tm[0] < 100: + tm = (tm[0] + (1900, 2000)[tm[0] < 70],) + tm[1:] return time.gmtime(rfc822.mktime_tz(tm)) - - # rfc822.py defines several time zones, but we define some extra ones. # 'ET' is equivalent to 'EST', etc. - -_additional_timezones = { - 'AT': -400, - 'ET': -500, - 'CT': -600, - 'MT': -700, - 'PT': -800, - } +_additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800} rfc822._timezones.update(_additional_timezones) registerDateHandler(_parse_date_rfc822) +def _parse_date_perforce(aDateString): + """parse a date in yyyy/mm/dd hh:mm:ss TTT format""" + # Fri, 2006/09/15 08:19:53 EDT + _my_date_pattern = re.compile( \ + r'(\w{,3}), (\d{,4})/(\d{,2})/(\d{2}) (\d{,2}):(\d{2}):(\d{2}) (\w{,3})') + + m = _my_date_pattern.search(aDateString) + if m is None: + return None + dow, year, month, day, hour, minute, second, tz = m.groups() + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + dateString = "%s, %s %s %s %s:%s:%s %s" % (dow, day, months[int(month) - 1], year, hour, minute, second, tz) + tm = rfc822.parsedate_tz(dateString) + if tm: + return time.gmtime(rfc822.mktime_tz(tm)) +registerDateHandler(_parse_date_perforce) def _parse_date(dateString): - """Parses a variety of date formats into a 9-tuple in GMT""" - + '''Parses a variety of date formats into a 9-tuple in GMT''' + if not dateString: + return None for handler in _date_handlers: try: date9tuple = handler(dateString) - if not date9tuple: - continue - if len(date9tuple) != 9: - if _debug: - sys.stderr.write('date handler function must return 9-tuple\n' - ) - raise ValueError, 'date handler function must return 9-tuple' - map(int, date9tuple) - return date9tuple - except Exception, e: - if _debug: - sys.stderr.write('%s raised %s\n' % (handler.__name__, - repr(e))) - pass + except (KeyError, OverflowError, ValueError): + continue + if not date9tuple: + continue + if len(date9tuple) != 9: + continue + return date9tuple return None - def _getCharacterEncoding(http_headers, xml_data): - """Get the character encoding of the XML document + '''Get the character encoding of the XML document http_headers is a dictionary xml_data is a raw string (not Unicode) This is so much trickier than it sounds, it's not even funny. @@ -3323,688 +3515,394 @@ Of course, none of this guarantees that we will be able to parse the feed in the declared character encoding (assuming it was declared correctly, which many are not). CJKCodecs and iconv_codec help a lot; you should definitely install them if you can. http://cjkpython.i18n.org/ - """ + ''' def _parseHTTPContentType(content_type): - """takes HTTP Content-Type header and returns (content type, charset) + '''takes HTTP Content-Type header and returns (content type, charset) If no charset is specified, returns (content type, '') If no content type is specified, returns ('', '') Both return parameters are guaranteed to be lowercase strings - """ - + ''' content_type = content_type or '' - (content_type, params) = cgi.parse_header(content_type) - return (content_type, params.get('charset', '').replace("'", '' - )) - - sniffed_xml_encoding = '' - xml_encoding = '' - true_encoding = '' - (http_content_type, http_encoding) = \ - _parseHTTPContentType(http_headers.get('content-type')) - + content_type, params = cgi.parse_header(content_type) + charset = params.get('charset', '').replace("'", "") + if not isinstance(charset, unicode): + charset = charset.decode('utf-8', 'ignore') + return content_type, charset + + sniffed_xml_encoding = u'' + xml_encoding = u'' + true_encoding = u'' + http_content_type, http_encoding = _parseHTTPContentType(http_headers.get('content-type', http_headers.get('Content-type'))) # Must sniff for non-ASCII-compatible character encodings before # searching for XML declaration. This heuristic is defined in # section F of the XML specification: # http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info - try: - if xml_data[:4] == '\x4c\x6f\xa7\x94': - + if xml_data[:4] == _l2bytes([0x4c, 0x6f, 0xa7, 0x94]): # EBCDIC - xml_data = _ebcdic_to_ascii(xml_data) - elif xml_data[:4] == '\x00\x3c\x00\x3f': - + elif xml_data[:4] == _l2bytes([0x00, 0x3c, 0x00, 0x3f]): # UTF-16BE - - sniffed_xml_encoding = 'utf-16be' + sniffed_xml_encoding = u'utf-16be' xml_data = unicode(xml_data, 'utf-16be').encode('utf-8') - elif len(xml_data) >= 4 and xml_data[:2] == '\xfe\xff'\ - and xml_data[2:4] != '\x00\x00': - + elif (len(xml_data) >= 4) and (xml_data[:2] == _l2bytes([0xfe, 0xff])) and (xml_data[2:4] != _l2bytes([0x00, 0x00])): # UTF-16BE with BOM - - sniffed_xml_encoding = 'utf-16be' + sniffed_xml_encoding = u'utf-16be' xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8') - elif xml_data[:4] == '\x3c\x00\x3f\x00': - + elif xml_data[:4] == _l2bytes([0x3c, 0x00, 0x3f, 0x00]): # UTF-16LE - - sniffed_xml_encoding = 'utf-16le' + sniffed_xml_encoding = u'utf-16le' xml_data = unicode(xml_data, 'utf-16le').encode('utf-8') - elif len(xml_data) >= 4 and xml_data[:2] == '\xff\xfe'\ - and xml_data[2:4] != '\x00\x00': - + elif (len(xml_data) >= 4) and (xml_data[:2] == _l2bytes([0xff, 0xfe])) and (xml_data[2:4] != _l2bytes([0x00, 0x00])): # UTF-16LE with BOM - - sniffed_xml_encoding = 'utf-16le' + sniffed_xml_encoding = u'utf-16le' xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8') - elif xml_data[:4] == '\x00\x00\x00\x3c': - + elif xml_data[:4] == _l2bytes([0x00, 0x00, 0x00, 0x3c]): # UTF-32BE - - sniffed_xml_encoding = 'utf-32be' + sniffed_xml_encoding = u'utf-32be' xml_data = unicode(xml_data, 'utf-32be').encode('utf-8') - elif xml_data[:4] == '\x3c\x00\x00\x00': - + elif xml_data[:4] == _l2bytes([0x3c, 0x00, 0x00, 0x00]): # UTF-32LE - - sniffed_xml_encoding = 'utf-32le' + sniffed_xml_encoding = u'utf-32le' xml_data = unicode(xml_data, 'utf-32le').encode('utf-8') - elif xml_data[:4] == '\x00\x00\xfe\xff': - + elif xml_data[:4] == _l2bytes([0x00, 0x00, 0xfe, 0xff]): # UTF-32BE with BOM - - sniffed_xml_encoding = 'utf-32be' + sniffed_xml_encoding = u'utf-32be' xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8') - elif xml_data[:4] == '\xff\xfe\x00\x00': - + elif xml_data[:4] == _l2bytes([0xff, 0xfe, 0x00, 0x00]): # UTF-32LE with BOM - - sniffed_xml_encoding = 'utf-32le' + sniffed_xml_encoding = u'utf-32le' xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8') - elif xml_data[:3] == '\xef\xbb\xbf': - + elif xml_data[:3] == _l2bytes([0xef, 0xbb, 0xbf]): # UTF-8 with BOM - - sniffed_xml_encoding = 'utf-8' + sniffed_xml_encoding = u'utf-8' xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8') else: - # ASCII-compatible - pass - xml_encoding_match = \ - re.compile('^<\?.*encoding=[\'"](.*?)[\'"].*\?>' - ).match(xml_data) - except: + xml_encoding_match = re.compile(_s2bytes('^<\?.*encoding=[\'"](.*?)[\'"].*\?>')).match(xml_data) + except UnicodeDecodeError: xml_encoding_match = None if xml_encoding_match: - xml_encoding = xml_encoding_match.groups()[0].lower() - if sniffed_xml_encoding and xml_encoding in ( - 'iso-10646-ucs-2', - 'ucs-2', - 'csunicode', - 'iso-10646-ucs-4', - 'ucs-4', - 'csucs4', - 'utf-16', - 'utf-32', - 'utf_16', - 'utf_32', - 'utf16', - 'u16', - ): + xml_encoding = xml_encoding_match.groups()[0].decode('utf-8').lower() + if sniffed_xml_encoding and (xml_encoding in (u'iso-10646-ucs-2', u'ucs-2', u'csunicode', u'iso-10646-ucs-4', u'ucs-4', u'csucs4', u'utf-16', u'utf-32', u'utf_16', u'utf_32', u'utf16', u'u16')): xml_encoding = sniffed_xml_encoding acceptable_content_type = 0 - application_content_types = ('application/xml', - 'application/xml-dtd', - 'application/xml-external-parsed-entity' - ) - text_content_types = ('text/xml', 'text/xml-external-parsed-entity') - if http_content_type in application_content_types\ - or http_content_type.startswith('application/')\ - and http_content_type.endswith('+xml'): - acceptable_content_type = 1 - true_encoding = http_encoding or xml_encoding or 'utf-8' - elif http_content_type in text_content_types\ - or http_content_type.startswith('text/')\ - and http_content_type.endswith('+xml'): - acceptable_content_type = 1 - true_encoding = http_encoding or 'us-ascii' - elif http_content_type.startswith('text/'): - true_encoding = http_encoding or 'us-ascii' - elif http_headers and not http_headers.has_key('content-type'): - true_encoding = xml_encoding or 'iso-8859-1' - else: - true_encoding = xml_encoding or 'utf-8' - return (true_encoding, http_encoding, xml_encoding, - sniffed_xml_encoding, acceptable_content_type) - + application_content_types = (u'application/xml', u'application/xml-dtd', u'application/xml-external-parsed-entity') + text_content_types = (u'text/xml', u'text/xml-external-parsed-entity') + if (http_content_type in application_content_types) or \ + (http_content_type.startswith(u'application/') and http_content_type.endswith(u'+xml')): + acceptable_content_type = 1 + true_encoding = http_encoding or xml_encoding or u'utf-8' + elif (http_content_type in text_content_types) or \ + (http_content_type.startswith(u'text/')) and http_content_type.endswith(u'+xml'): + acceptable_content_type = 1 + true_encoding = http_encoding or u'us-ascii' + elif http_content_type.startswith(u'text/'): + true_encoding = http_encoding or u'us-ascii' + elif http_headers and (not (http_headers.has_key('content-type') or http_headers.has_key('Content-type'))): + true_encoding = xml_encoding or u'iso-8859-1' + else: + true_encoding = xml_encoding or u'utf-8' + # some feeds claim to be gb2312 but are actually gb18030. + # apparently MSIE and Firefox both do the following switch: + if true_encoding.lower() == u'gb2312': + true_encoding = u'gb18030' + return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type def _toUTF8(data, encoding): - """Changes an XML data stream on the fly to specify a new encoding + '''Changes an XML data stream on the fly to specify a new encoding data is a raw sequence of bytes (not Unicode) that is presumed to be in %encoding already encoding is a string recognized by encodings.aliases - """ - - if _debug: - sys.stderr.write('entering _toUTF8, trying encoding %s\n' - % encoding) - + ''' # strip Byte Order Mark (if present) - - if len(data) >= 4 and data[:2] == '\xfe\xff' and data[2:4]\ - != '\x00\x00': - if _debug: - sys.stderr.write('stripping BOM\n') - if encoding != 'utf-16be': - sys.stderr.write('trying utf-16be instead\n') + if (len(data) >= 4) and (data[:2] == _l2bytes([0xfe, 0xff])) and (data[2:4] != _l2bytes([0x00, 0x00])): encoding = 'utf-16be' data = data[2:] - elif len(data) >= 4 and data[:2] == '\xff\xfe' and data[2:4]\ - != '\x00\x00': - if _debug: - sys.stderr.write('stripping BOM\n') - if encoding != 'utf-16le': - sys.stderr.write('trying utf-16le instead\n') + elif (len(data) >= 4) and (data[:2] == _l2bytes([0xff, 0xfe])) and (data[2:4] != _l2bytes([0x00, 0x00])): encoding = 'utf-16le' data = data[2:] - elif data[:3] == '\xef\xbb\xbf': - if _debug: - sys.stderr.write('stripping BOM\n') - if encoding != 'utf-8': - sys.stderr.write('trying utf-8 instead\n') + elif data[:3] == _l2bytes([0xef, 0xbb, 0xbf]): encoding = 'utf-8' data = data[3:] - elif data[:4] == '\x00\x00\xfe\xff': - if _debug: - sys.stderr.write('stripping BOM\n') - if encoding != 'utf-32be': - sys.stderr.write('trying utf-32be instead\n') + elif data[:4] == _l2bytes([0x00, 0x00, 0xfe, 0xff]): encoding = 'utf-32be' data = data[4:] - elif data[:4] == '\xff\xfe\x00\x00': - if _debug: - sys.stderr.write('stripping BOM\n') - if encoding != 'utf-32le': - sys.stderr.write('trying utf-32le instead\n') + elif data[:4] == _l2bytes([0xff, 0xfe, 0x00, 0x00]): encoding = 'utf-32le' data = data[4:] newdata = unicode(data, encoding) - if _debug: - sys.stderr.write('successfully converted %s data to unicode\n' - % encoding) declmatch = re.compile('^<\?xml[^>]*?>') newdecl = '''''' if declmatch.search(newdata): newdata = declmatch.sub(newdecl, newdata) else: newdata = newdecl + u'\n' + newdata return newdata.encode('utf-8') - def _stripDoctype(data): - """Strips DOCTYPE from XML document, returns (rss_version, stripped_data) + '''Strips DOCTYPE from XML document, returns (rss_version, stripped_data) rss_version may be 'rss091n' or None stripped_data is the same XML document, minus the DOCTYPE - """ - - entity_pattern = re.compile(r']*?)>', re.MULTILINE) - data = entity_pattern.sub('', data) - doctype_pattern = re.compile(r']*?)>', re.MULTILINE) - doctype_results = doctype_pattern.findall(data) - doctype = doctype_results and doctype_results[0] or '' - if doctype.lower().count('netscape'): - version = 'rss091n' + ''' + start = re.search(_s2bytes('<\w'), data) + start = start and start.start() or -1 + head,data = data[:start+1], data[start+1:] + + entity_pattern = re.compile(_s2bytes(r'^\s*]*?)>'), re.MULTILINE) + entity_results=entity_pattern.findall(head) + head = entity_pattern.sub(_s2bytes(''), head) + doctype_pattern = re.compile(_s2bytes(r'^\s*]*?)>'), re.MULTILINE) + doctype_results = doctype_pattern.findall(head) + doctype = doctype_results and doctype_results[0] or _s2bytes('') + if doctype.lower().count(_s2bytes('netscape')): + version = u'rss091n' else: version = None - data = doctype_pattern.sub('', data) - return (version, data) - - -def parse( - url_file_stream_or_string, - etag=None, - modified=None, - agent=None, - referrer=None, - handlers=[], - ): - """Parse a feed from a URL, file, stream, or string""" + + # only allow in 'safe' inline entity definitions + replacement=_s2bytes('') + if len(doctype_results)==1 and entity_results: + safe_pattern=re.compile(_s2bytes('\s+(\w+)\s+"(&#\w+;|[^&"]*)"')) + safe_entities=filter(lambda e: safe_pattern.match(e),entity_results) + if safe_entities: + replacement=_s2bytes('\n \n]>') + data = doctype_pattern.sub(replacement, head) + data + + return version, data, dict(replacement and [(k.decode('utf-8'), v.decode('utf-8')) for k, v in safe_pattern.findall(replacement)]) + +def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=None, request_headers=None, response_headers=None): + '''Parse a feed from a URL, file, stream, or string. + + request_headers, if given, is a dict from http header name to value to add + to the request; this overrides internally generated values. + ''' + + if handlers is None: + handlers = [] + if request_headers is None: + request_headers = {} + if response_headers is None: + response_headers = {} result = FeedParserDict() result['feed'] = FeedParserDict() result['entries'] = [] - if _XML_AVAILABLE: - result['bozo'] = 0 - if type(handlers) == types.InstanceType: + result['bozo'] = 0 + if not isinstance(handlers, list): handlers = [handlers] try: - f = _open_resource( - url_file_stream_or_string, - etag, - modified, - agent, - referrer, - handlers, - ) + f = _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers, request_headers) data = f.read() except Exception, e: result['bozo'] = 1 result['bozo_exception'] = e - data = '' + data = None f = None + if hasattr(f, 'headers'): + result['headers'] = dict(f.headers) + # overwrite existing headers using response_headers + if 'headers' in result: + result['headers'].update(response_headers) + elif response_headers: + result['headers'] = copy.deepcopy(response_headers) + # if feed is gzip-compressed, decompress it - - if f and data and hasattr(f, 'headers'): - if gzip and f.headers.get('content-encoding', '') == 'gzip': + if f and data and 'headers' in result: + if gzip and 'gzip' in (result['headers'].get('content-encoding'), result['headers'].get('Content-Encoding')): try: data = gzip.GzipFile(fileobj=_StringIO(data)).read() - except Exception, e: - + except (IOError, struct.error), e: + # IOError can occur if the gzip header is bad + # struct.error can occur if the data is damaged # Some feeds claim to be gzipped but they're not, so # we get garbage. Ideally, we should re-request the # feed without the 'Accept-encoding: gzip' header, # but we don't. - - result['bozo'] = 1 - result['bozo_exception'] = e - data = '' - elif zlib and f.headers.get('content-encoding', '')\ - == 'deflate': - try: - data = zlib.decompress(data, -zlib.MAX_WBITS) - except Exception, e: - result['bozo'] = 1 - result['bozo_exception'] = e - data = '' - - # save HTTP headers - - if hasattr(f, 'info'): - info = f.info() - result['etag'] = info.getheader('ETag') - last_modified = info.getheader('Last-Modified') - if last_modified: - result['modified'] = _parse_date(last_modified) - if hasattr(f, 'url'): - result['href'] = f.url + result['bozo'] = 1 + result['bozo_exception'] = e + data = None + elif zlib and 'deflate' in (result['headers'].get('content-encoding'), result['headers'].get('Content-Encoding')): + try: + data = zlib.decompress(data) + except zlib.error, e: + result['bozo'] = 1 + result['bozo_exception'] = e + data = None + + # save HTTP headers + if 'headers' in result: + if 'etag' in result['headers'] or 'ETag' in result['headers']: + etag = result['headers'].get('etag', result['headers'].get('ETag', u'')) + if not isinstance(etag, unicode): + etag = etag.decode('utf-8', 'ignore') + if etag: + result['etag'] = etag + if 'last-modified' in result['headers'] or 'Last-Modified' in result['headers']: + modified = result['headers'].get('last-modified', result['headers'].get('Last-Modified')) + if modified: + result['modified'] = _parse_date(modified) + if hasattr(f, 'url'): + if not isinstance(f.url, unicode): + result['href'] = f.url.decode('utf-8', 'ignore') + else: + result['href'] = f.url result['status'] = 200 if hasattr(f, 'status'): result['status'] = f.status - if hasattr(f, 'headers'): - result['headers'] = f.headers.dict if hasattr(f, 'close'): f.close() + if data is None: + return result + # there are four encodings to keep track of: # - http_encoding is the encoding declared in the Content-Type HTTP header # - xml_encoding is the encoding declared in the ; changed -# project name -# 2.5 - 7/25/2003 - MAP - changed to Python license (all contributors agree); -# removed unnecessary urllib code -- urllib2 should always be available anyway; -# return actual url, status, and full HTTP headers (as result['url'], -# result['status'], and result['headers']) if parsing a remote feed over HTTP -- -# this should pass all the HTTP tests at ; -# added the latest namespace-of-the-week for RSS 2.0 -# 2.5.1 - 7/26/2003 - RMK - clear opener.addheaders so we only send our custom -# User-Agent (otherwise urllib2 sends two, which confuses some servers) -# 2.5.2 - 7/28/2003 - MAP - entity-decode inline xml properly; added support for -# inline and as used in some RSS 2.0 feeds -# 2.5.3 - 8/6/2003 - TvdV - patch to track whether we're inside an image or -# textInput, and also to return the character encoding (if specified) -# 2.6 - 1/1/2004 - MAP - dc:author support (MarekK); fixed bug tracking -# nested divs within content (JohnD); fixed missing sys import (JohanS); -# fixed regular expression to capture XML character encoding (Andrei); -# added support for Atom 0.3-style links; fixed bug with textInput tracking; -# added support for cloud (MartijnP); added support for multiple -# category/dc:subject (MartijnP); normalize content model: 'description' gets -# description (which can come from description, summary, or full content if no -# description), 'content' gets dict of base/language/type/value (which can come -# from content:encoded, xhtml:body, content, or fullitem); -# fixed bug matching arbitrary Userland namespaces; added xml:base and xml:lang -# tracking; fixed bug tracking unknown tags; fixed bug tracking content when -# element is not in default namespace (like Pocketsoap feed); -# resolve relative URLs in link, guid, docs, url, comments, wfw:comment, -# wfw:commentRSS; resolve relative URLs within embedded HTML markup in -# description, xhtml:body, content, content:encoded, title, subtitle, -# summary, info, tagline, and copyright; added support for pingback and -# trackback namespaces -# 2.7 - 1/5/2004 - MAP - really added support for trackback and pingback -# namespaces, as opposed to 2.6 when I said I did but didn't really; -# sanitize HTML markup within some elements; added mxTidy support (if -# installed) to tidy HTML markup within some elements; fixed indentation -# bug in _parse_date (FazalM); use socket.setdefaulttimeout if available -# (FazalM); universal date parsing and normalization (FazalM): 'created', modified', -# 'issued' are parsed into 9-tuple date format and stored in 'created_parsed', -# 'modified_parsed', and 'issued_parsed'; 'date' is duplicated in 'modified' -# and vice-versa; 'date_parsed' is duplicated in 'modified_parsed' and vice-versa -# 2.7.1 - 1/9/2004 - MAP - fixed bug handling " and '. fixed memory -# leak not closing url opener (JohnD); added dc:publisher support (MarekK); -# added admin:errorReportsTo support (MarekK); Python 2.1 dict support (MarekK) -# 2.7.4 - 1/14/2004 - MAP - added workaround for improperly formed
    tags in -# encoded HTML (skadz); fixed unicode handling in normalize_attrs (ChrisL); -# fixed relative URI processing for guid (skadz); added ICBM support; added -# base64 support -# 2.7.5 - 1/15/2004 - MAP - added workaround for malformed DOCTYPE (seen on many -# blogspot.com sites); added _debug variable -# 2.7.6 - 1/16/2004 - MAP - fixed bug with StringIO importing -# 3.0b3 - 1/23/2004 - MAP - parse entire feed with real XML parser (if available); -# added several new supported namespaces; fixed bug tracking naked markup in -# description; added support for enclosure; added support for source; re-added -# support for cloud which got dropped somehow; added support for expirationDate -# 3.0b4 - 1/26/2004 - MAP - fixed xml:lang inheritance; fixed multiple bugs tracking -# xml:base URI, one for documents that don't define one explicitly and one for -# documents that define an outer and an inner xml:base that goes out of scope -# before the end of the document -# 3.0b5 - 1/26/2004 - MAP - fixed bug parsing multiple links at feed level -# 3.0b6 - 1/27/2004 - MAP - added feed type and version detection, result['version'] -# will be one of SUPPORTED_VERSIONS.keys() or empty string if unrecognized; -# added support for creativeCommons:license and cc:license; added support for -# full Atom content model in title, tagline, info, copyright, summary; fixed bug -# with gzip encoding (not always telling server we support it when we do) -# 3.0b7 - 1/28/2004 - MAP - support Atom-style author element in author_detail -# (dictionary of 'name', 'url', 'email'); map author to author_detail if author -# contains name + email address -# 3.0b8 - 1/28/2004 - MAP - added support for contributor -# 3.0b9 - 1/29/2004 - MAP - fixed check for presence of dict function; added -# support for summary -# 3.0b10 - 1/31/2004 - MAP - incorporated ISO-8601 date parsing routines from -# xml.util.iso8601 -# 3.0b11 - 2/2/2004 - MAP - added 'rights' to list of elements that can contain -# dangerous markup; fiddled with decodeEntities (not right); liberalized -# date parsing even further -# 3.0b12 - 2/6/2004 - MAP - fiddled with decodeEntities (still not right); -# added support to Atom 0.2 subtitle; added support for Atom content model -# in copyright; better sanitizing of dangerous HTML elements with end tags -# (script, frameset) -# 3.0b13 - 2/8/2004 - MAP - better handling of empty HTML tags (br, hr, img, -# etc.) in embedded markup, in either HTML or XHTML form (
    ,
    ,
    ) -# 3.0b14 - 2/8/2004 - MAP - fixed CDATA handling in non-wellformed feeds under -# Python 2.1 -# 3.0b15 - 2/11/2004 - MAP - fixed bug resolving relative links in wfw:commentRSS; -# fixed bug capturing author and contributor URL; fixed bug resolving relative -# links in author and contributor URL; fixed bug resolvin relative links in -# generator URL; added support for recognizing RSS 1.0; passed Simon Fell's -# namespace tests, and included them permanently in the test suite with his -# permission; fixed namespace handling under Python 2.1 -# 3.0b16 - 2/12/2004 - MAP - fixed support for RSS 0.90 (broken in b15) -# 3.0b17 - 2/13/2004 - MAP - determine character encoding as per RFC 3023 -# 3.0b18 - 2/17/2004 - MAP - always map description to summary_detail (Andrei); -# use libxml2 (if available) -# 3.0b19 - 3/15/2004 - MAP - fixed bug exploding author information when author -# name was in parentheses; removed ultra-problematic mxTidy support; patch to -# workaround crash in PyXML/expat when encountering invalid entities -# (MarkMoraes); support for textinput/textInput -# 3.0b20 - 4/7/2004 - MAP - added CDF support -# 3.0b21 - 4/14/2004 - MAP - added Hot RSS support -# 3.0b22 - 4/19/2004 - MAP - changed 'channel' to 'feed', 'item' to 'entries' in -# results dict; changed results dict to allow getting values with results.key -# as well as results[key]; work around embedded illformed HTML with half -# a DOCTYPE; work around malformed Content-Type header; if character encoding -# is wrong, try several common ones before falling back to regexes (if this -# works, bozo_exception is set to CharacterEncodingOverride); fixed character -# encoding issues in BaseHTMLProcessor by tracking encoding and converting -# from Unicode to raw strings before feeding data to sgmllib.SGMLParser; -# convert each value in results to Unicode (if possible), even if using -# regex-based parsing -# 3.0b23 - 4/21/2004 - MAP - fixed UnicodeDecodeError for feeds that contain -# high-bit characters in attributes in embedded HTML in description (thanks -# Thijs van de Vossen); moved guid, date, and date_parsed to mapped keys in -# FeedParserDict; tweaked FeedParserDict.has_key to return True if asking -# about a mapped key -# 3.0fc1 - 4/23/2004 - MAP - made results.entries[0].links[0] and -# results.entries[0].enclosures[0] into FeedParserDict; fixed typo that could -# cause the same encoding to be tried twice (even if it failed the first time); -# fixed DOCTYPE stripping when DOCTYPE contained entity declarations; -# better textinput and image tracking in illformed RSS 1.0 feeds -# 3.0fc2 - 5/10/2004 - MAP - added and passed Sam's amp tests; added and passed -# my blink tag tests -# 3.0fc3 - 6/18/2004 - MAP - fixed bug in _changeEncodingDeclaration that -# failed to parse utf-16 encoded feeds; made source into a FeedParserDict; -# duplicate admin:generatorAgent/@rdf:resource in generator_detail.url; -# added support for image; refactored parse() fallback logic to try other -# encodings if SAX parsing fails (previously it would only try other encodings -# if re-encoding failed); remove unichr madness in normalize_attrs now that -# we're properly tracking encoding in and out of BaseHTMLProcessor; set -# feed.language from root-level xml:lang; set entry.id from rdf:about; -# send Accept header -# 3.0 - 6/21/2004 - MAP - don't try iso-8859-1 (can't distinguish between -# iso-8859-1 and windows-1252 anyway, and most incorrectly marked feeds are -# windows-1252); fixed regression that could cause the same encoding to be -# tried twice (even if it failed the first time) -# 3.0.1 - 6/22/2004 - MAP - default to us-ascii for all text/* content types; -# recover from malformed content-type header parameter with no equals sign -# ('text/xml; charset:iso-8859-1') -# 3.1 - 6/28/2004 - MAP - added and passed tests for converting HTML entities -# to Unicode equivalents in illformed feeds (aaronsw); added and -# passed tests for converting character entities to Unicode equivalents -# in illformed feeds (aaronsw); test for valid parsers when setting -# XML_AVAILABLE; make version and encoding available when server returns -# a 304; add handlers parameter to pass arbitrary urllib2 handlers (like -# digest auth or proxy support); add code to parse username/password -# out of url and send as basic authentication; expose downloading-related -# exceptions in bozo_exception (aaronsw); added __contains__ method to -# FeedParserDict (aaronsw); added publisher_detail (aaronsw) -# 3.2 - 7/3/2004 - MAP - use cjkcodecs and iconv_codec if available; always -# convert feed to UTF-8 before passing to XML parser; completely revamped -# logic for determining character encoding and attempting XML parsing -# (much faster); increased default timeout to 20 seconds; test for presence -# of Location header on redirects; added tests for many alternate character -# encodings; support various EBCDIC encodings; support UTF-16BE and -# UTF16-LE with or without a BOM; support UTF-8 with a BOM; support -# UTF-32BE and UTF-32LE with or without a BOM; fixed crashing bug if no -# XML parsers are available; added support for 'Content-encoding: deflate'; -# send blank 'Accept-encoding: ' header if neither gzip nor zlib modules -# are available -# 3.3 - 7/15/2004 - MAP - optimize EBCDIC to ASCII conversion; fix obscure -# problem tracking xml:base and xml:lang if element declares it, child -# doesn't, first grandchild redeclares it, and second grandchild doesn't; -# refactored date parsing; defined public registerDateHandler so callers -# can add support for additional date formats at runtime; added support -# for OnBlog, Nate, MSSQL, Greek, and Hungarian dates (ytrewq1); added -# zopeCompatibilityHack() which turns FeedParserDict into a regular -# dictionary, required for Zope compatibility, and also makes command- -# line debugging easier because pprint module formats real dictionaries -# better than dictionary-like objects; added NonXMLContentType exception, -# which is stored in bozo_exception when a feed is served with a non-XML -# media type such as 'text/plain'; respect Content-Language as default -# language if not xml:lang is present; cloud dict is now FeedParserDict; -# generator dict is now FeedParserDict; better tracking of xml:lang, -# including support for xml:lang='' to unset the current language; -# recognize RSS 1.0 feeds even when RSS 1.0 namespace is not the default -# namespace; don't overwrite final status on redirects (scenarios: -# redirecting to a URL that returns 304, redirecting to a URL that -# redirects to another URL with a different type of redirect); add -# support for HTTP 303 redirects -# 4.0 - MAP - support for relative URIs in xml:base attribute; fixed -# encoding issue with mxTidy (phopkins); preliminary support for RFC 3229; -# support for Atom 1.0; support for iTunes extensions; new 'tags' for -# categories/keywords/etc. as array of dict -# {'term': term, 'scheme': scheme, 'label': label} to match Atom 1.0 -# terminology; parse RFC 822-style dates with no time; lots of other -# bug fixes -# 4.1 - MAP - removed socket timeout; added support for chardet library - Index: gluon/contrib/gae_retry.py ================================================================== --- gluon/contrib/gae_retry.py +++ gluon/contrib/gae_retry.py @@ -82,6 +82,7 @@ time.sleep(sleep) setattr(wrapper, '_autoretry_datastore_timeouts', False) if getattr(wrapped, '_autoretry_datastore_timeouts', True): apiproxy_stub_map.MakeSyncCall = wrapper + Index: gluon/contrib/gateways/__init__.py ================================================================== --- gluon/contrib/gateways/__init__.py +++ gluon/contrib/gateways/__init__.py @@ -1,1 +1,2 @@ + Index: gluon/contrib/gateways/fcgi.py ================================================================== --- gluon/contrib/gateways/fcgi.py +++ gluon/contrib/gateways/fcgi.py @@ -1327,5 +1327,6 @@ yield '
    \n' \ '\n' WSGIServer(test_app).run() + Index: gluon/contrib/generics.py ================================================================== --- gluon/contrib/generics.py +++ gluon/contrib/generics.py @@ -59,6 +59,7 @@ # try use latex and pdflatex if os.system('which pdflatex > /dev/null')==0: return pdflatex_from_html(html) else: return pyfpdf_from_html(html) + Index: gluon/contrib/gql.py ================================================================== --- gluon/contrib/gql.py +++ gluon/contrib/gql.py @@ -1,6 +1,7 @@ # this file exists for backward compatibility __all__ = ['DAL','Field','drivers','gae'] from gluon.dal import DAL, Field, Table, Query, Set, Expression, Row, Rows, drivers, BaseAdapter, SQLField, SQLTable, SQLXorable, SQLQuery, SQLSet, SQLRows, SQLStorage, SQLDB, GQLDB, SQLALL, SQLCustomType, gae + Index: gluon/contrib/login_methods/__init__.py ================================================================== --- gluon/contrib/login_methods/__init__.py +++ gluon/contrib/login_methods/__init__.py @@ -1,1 +1,2 @@ + Index: gluon/contrib/login_methods/basic_auth.py ================================================================== --- gluon/contrib/login_methods/basic_auth.py +++ gluon/contrib/login_methods/basic_auth.py @@ -20,5 +20,6 @@ urllib2.urlopen(request) return True except (urllib2.URLError, urllib2.HTTPError): return False return basic_login_aux + Index: gluon/contrib/login_methods/cas_auth.py ================================================================== --- gluon/contrib/login_methods/cas_auth.py +++ gluon/contrib/login_methods/cas_auth.py @@ -55,16 +55,16 @@ self.maps=maps self.casversion = casversion self.casusername = casusername http_host=current.request.env.http_x_forwarded_host if not http_host: http_host=current.request.env.http_host - if current.request.env.wsgi_url_scheme in [ 'https', 'HTTPS' ]: - scheme = 'https' - else: - scheme = 'http' - self.cas_my_url='%s://%s%s'%( scheme, http_host, current.request.env.path_info ) - + if current.request.env.wsgi_url_scheme in [ 'https', 'HTTPS' ]: + scheme = 'https' + else: + scheme = 'http' + self.cas_my_url='%s://%s%s'%( scheme, http_host, current.request.env.path_info ) + def login_url( self, next = "/" ): current.session.token=self._CAS_login() return next def logout_url( self, next = "/" ): current.session.token=None @@ -127,5 +127,6 @@ exposed CAS.logout() redirects to the CAS logout page """ import urllib redirect("%s?service=%s" % (self.cas_logout_url,self.cas_my_url)) + Index: gluon/contrib/login_methods/email_auth.py ================================================================== --- gluon/contrib/login_methods/email_auth.py +++ gluon/contrib/login_methods/email_auth.py @@ -32,5 +32,6 @@ except: if server: server.quit() return False return email_auth_aux + Index: gluon/contrib/login_methods/extended_login_form.py ================================================================== --- gluon/contrib/login_methods/extended_login_form.py +++ gluon/contrib/login_methods/extended_login_form.py @@ -98,5 +98,6 @@ form = DIV(self.auth()) self.auth.settings.login_form = self form.components.append(self.alt_login_form.login_form()) return form + Index: gluon/contrib/login_methods/gae_google_account.py ================================================================== --- gluon/contrib/login_methods/gae_google_account.py +++ gluon/contrib/login_methods/gae_google_account.py @@ -33,5 +33,6 @@ def get_user(self): user = users.get_current_user() if user: return dict(nickname=user.nickname(), email=user.email(), user_id=user.user_id(), source="google account") + Index: gluon/contrib/login_methods/ldap_auth.py ================================================================== --- gluon/contrib/login_methods/ldap_auth.py +++ gluon/contrib/login_methods/ldap_auth.py @@ -166,5 +166,6 @@ return False if filterstr[0] == '(' and filterstr[-1] == ')': # rfc4515 syntax filterstr = filterstr[1:-1] # parens added again where used return ldap_auth_aux + Index: gluon/contrib/login_methods/linkedin_account.py ================================================================== --- gluon/contrib/login_methods/linkedin_account.py +++ gluon/contrib/login_methods/linkedin_account.py @@ -45,6 +45,7 @@ profile = self.api.GetProfile() profile = self.api.GetProfile(profile).public_url = "http://www.linkedin.com/in/ozgurv" return dict(first_name = profile.first_name, last_name = profile.last_name, username = profile.id) + Index: gluon/contrib/login_methods/loginza.py ================================================================== --- gluon/contrib/login_methods/loginza.py +++ gluon/contrib/login_methods/loginza.py @@ -107,5 +107,6 @@ _style="width:359px;height:300px;") else: form = DIV(A(self.prompt, _href=LOGINZA_URL % (self.language, self.token_url), _class="loginza"), SCRIPT(_src="https://s3-eu-west-1.amazonaws.com/s1.loginza.ru/js/widget.js", _type="text/javascript")) return form + Index: gluon/contrib/login_methods/oauth10a_account.py ================================================================== --- gluon/contrib/login_methods/oauth10a_account.py +++ gluon/contrib/login_methods/oauth10a_account.py @@ -183,7 +183,8 @@ raise HTTP(307, "You are not authenticated: you are being redirected to the authentication server", Location=auth_request_url) return None + Index: gluon/contrib/login_methods/oauth20_account.py ================================================================== --- gluon/contrib/login_methods/oauth20_account.py +++ gluon/contrib/login_methods/oauth20_account.py @@ -202,5 +202,6 @@ else: self.session.code = self.request.vars.code self.accessToken() return self.session.code return None + Index: gluon/contrib/login_methods/openid_auth.py ================================================================== --- gluon/contrib/login_methods/openid_auth.py +++ gluon/contrib/login_methods/openid_auth.py @@ -626,6 +626,7 @@ This method should be run periodically to free the db from expired nonce and association entries. """ return self.cleanupNonces(), self.cleanupAssociations() + Index: gluon/contrib/login_methods/pam_auth.py ================================================================== --- gluon/contrib/login_methods/pam_auth.py +++ gluon/contrib/login_methods/pam_auth.py @@ -9,5 +9,6 @@ def pam_auth_aux(username, password): return authenticate(username, password) return pam_auth_aux + Index: gluon/contrib/login_methods/rpx_account.py ================================================================== --- gluon/contrib/login_methods/rpx_account.py +++ gluon/contrib/login_methods/rpx_account.py @@ -9,13 +9,14 @@ This file contains code to allow using RPXNow.com (now Jainrain.com) services with web2py """ +import os import re import urllib -from gluon.html import * +from gluon import * from gluon.tools import fetch from gluon.storage import Storage import gluon.contrib.simplejson as json class RPXAccount(object): @@ -50,23 +51,24 @@ self.language = language self.profile = None self.prompt = prompt self.on_login_failure = on_login_failure self.mappings = Storage() - - self.mappings.Facebook = lambda profile:\ + + dn = {'givenName':'','familyName':''} + self.mappings.Facebook = lambda profile, dn=dn:\ dict(registration_id = profile.get("identifier",""), username = profile.get("preferredUsername",""), - email = profile.get("email",""), - first_name = profile.get("name","").get("givenName",""), - last_name = profile.get("name","").get("familyName","")) - self.mappings.Google = lambda profile:\ + email = profile.get("email",""), + first_name = profile.get("name",dn).get("givenName",""), + last_name = profile.get("name",dn).get("familyName","")) + self.mappings.Google = lambda profile, dn=dn:\ dict(registration_id=profile.get("identifier",""), username=profile.get("preferredUsername",""), email=profile.get("email",""), - first_name=profile.get("name","").get("givenName",""), - last_name=profile.get("name","").get("familyName","")) + first_name=profile.get("name",dn).get("givenName",""), + last_name=profile.get("name",dn).get("familyName","")) self.mappings.default = lambda profile:\ dict(registration_id=profile.get("identifier",""), username=profile.get("preferredUsername",""), email=profile.get("email",""), first_name=profile.get("preferredUsername",""), @@ -109,5 +111,17 @@ "RPXNOW.realm = '%s';" % self.domain, "RPXNOW.token_url = '%s';" % self.token_url, "RPXNOW.show();", _type="text/javascript")) return rpxform + +def use_janrain(auth,filename='private/janrain.key',**kwargs): + path = os.path.join(current.request.folder,filename) + if os.path.exists(path): + request = current.request + domain,key = open(path,'r').read().strip().split(':') + host = current.request.env.http_host + url = "http://%s/%s/default/user/login" % (host,request.application) + auth.settings.actions_disabled = \ + ['register','change_password','request_reset_password'] + auth.settings.login_form = RPXAccount( + request, api_key=key,domain=domain, url = url,**kwargs) ADDED gluon/contrib/login_methods/x509_auth.py Index: gluon/contrib/login_methods/x509_auth.py ================================================================== --- gluon/contrib/login_methods/x509_auth.py +++ gluon/contrib/login_methods/x509_auth.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Written by Michele Comitini +License: GPL v3 + +Adds support for x509 authentication. + +""" + +from gluon.globals import current +from gluon.storage import Storage +from gluon.http import HTTP,redirect + +#requires M2Crypto +from M2Crypto import X509 + + + +class X509Auth(object): + """ + Login using x509 cert from client. + + """ + + + + def __init__(self): + self.request = current.request + self.ssl_client_raw_cert = self.request.env.ssl_client_raw_cert + + # rebuild the certificate passed by the env + # this is double work, but it is the only way + # since we cannot access the web server ssl engine directly + + if self.ssl_client_raw_cert: + + x509=X509.load_cert_string(self.ssl_client_raw_cert, X509.FORMAT_PEM) + # extract it from the cert + self.serial = self.request.env.ssl_client_serial or ('%x' % x509.get_serial_number()).upper() + + + subject = x509.get_subject() + + # Reordering the subject map to a usable Storage map + # this allows us a cleaner syntax: + # cn = self.subject.cn + self.subject = Storage(filter(None, + map(lambda x: + (x,map(lambda y: + y.get_data().as_text(), + subject.get_entries_by_nid(subject.nid[x]))), + subject.nid.keys()))) + + + + def login_form(self, **args): + raise HTTP(403,'Login not allowed. No valid x509 crentials') + + + + def login_url(self, next="/"): + raise HTTP(403,'Login not allowed. No valid x509 crentials') + + + + + def logout_url(self, next="/"): + return next + + def get_user(self): + '''Returns the user info contained in the certificate. + ''' + + # We did not get the client cert? + if not self.ssl_client_raw_cert: + return None + + # Try to reconstruct some useful info for web2py auth machinery + + p = profile = dict() + + username = p['username'] = self.subject.CN or self.subject.commonName + p['first_name'] = self.subject.givenName or username + p['last_name'] = self.subject.surname + p['email'] = self.subject.Email or self.subject.emailAddress + + # IMPORTANT WE USE THE CERT SERIAL AS UNIQUE KEY FOR THE USER + p['registration_id'] = self.serial + + # If the auth table has a field certificate it will be used to + # save a PEM encoded copy of the user certificate. + + p['certificate'] = self.ssl_client_raw_cert + + return profile + + Index: gluon/contrib/markdown/__init__.py ================================================================== --- gluon/contrib/markdown/__init__.py +++ gluon/contrib/markdown/__init__.py @@ -12,5 +12,6 @@ text = text.decode(encoding,'replace') return XML(markdown(text,extras=extras, safe_mode=safe_mode, html4tags=html4tags)\ .encode(encoding,'xmlcharrefreplace'),**attributes) + Index: gluon/contrib/markdown/markdown2.py ================================================================== --- gluon/contrib/markdown/markdown2.py +++ gluon/contrib/markdown/markdown2.py @@ -1884,6 +1884,7 @@ print "==== match? %r ====" % (norm_perl_html == norm_html) if __name__ == "__main__": sys.exit( main(sys.argv) ) + Index: gluon/contrib/markmin/__init__.py ================================================================== --- gluon/contrib/markmin/__init__.py +++ gluon/contrib/markmin/__init__.py @@ -1,1 +1,2 @@ + Index: gluon/contrib/markmin/markmin2html.py ================================================================== --- gluon/contrib/markmin/markmin2html.py +++ gluon/contrib/markmin/markmin2html.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python # created my Massimo Di Pierro +#!/usr/bin/env python +# created my Massimo Di Pierro # license MIT/BSD/GPL import re import cgi __all__ = ['render', 'markmin2html'] @@ -395,11 +396,13 @@ # the << indicates that there should NOT be a new paragraph # META indicates a code block therefore no new paragraph ############################################################# items = [item.strip() for item in text.split('\n\n')] if sep=='p': - text = ''.join(p[:2]!='<<' and p!=META and '

    %s

    '%p or '%s'%p for p in items if p) + text = ''.join( + (p[:2]!='<<' and p!=META and '

    %s

    '%p or '%s'%p) \ + for p in items if p.strip()) elif sep=='br': text = '
    '.join(items) ############################################################# # finally get rid of << @@ -451,5 +454,6 @@ print ''+markmin2html(fargv.read())+'' finally: fargv.close() else: doctest.testmod() + Index: gluon/contrib/markmin/markmin2latex.py ================================================================== --- gluon/contrib/markmin/markmin2latex.py +++ gluon/contrib/markmin/markmin2latex.py @@ -279,6 +279,7 @@ if options.one: output=output.replace(r'\section*{',r'\chapter*{') output=output.replace(r'\section{',r'\chapter{') output=output.replace(r'subsection{',r'section{') print output + Index: gluon/contrib/markmin/markmin2pdf.py ================================================================== --- gluon/contrib/markmin/markmin2pdf.py +++ gluon/contrib/markmin/markmin2pdf.py @@ -125,5 +125,6 @@ print 'WARNGINS:'+'\n'.join(warnings) else: print data else: doctest.testmod() + Index: gluon/contrib/memdb.py ================================================================== --- gluon/contrib/memdb.py +++ gluon/contrib/memdb.py @@ -902,6 +902,7 @@ SQLStorage = DALStorage if __name__ == '__main__': import doctest doctest.testmod() + Index: gluon/contrib/pam.py ================================================================== --- gluon/contrib/pam.py +++ gluon/contrib/pam.py @@ -119,6 +119,7 @@ return retval == 0 if __name__ == "__main__": import getpass print authenticate(getpass.getuser(), getpass.getpass()) + Index: gluon/contrib/populate.py ================================================================== --- gluon/contrib/populate.py +++ gluon/contrib/populate.py cannot compute difference between binary files Index: gluon/contrib/pyfpdf/__init__.py ================================================================== --- gluon/contrib/pyfpdf/__init__.py +++ gluon/contrib/pyfpdf/__init__.py @@ -1,4 +1,5 @@ from fpdf import FPDF from html import HTMLMixin from template import Template + Index: gluon/contrib/pyfpdf/designer.py ================================================================== --- gluon/contrib/pyfpdf/designer.py +++ gluon/contrib/pyfpdf/designer.py @@ -729,7 +729,8 @@ app = wx.PySimpleApp() ogl.OGLInitialize() frame = AppFrame() app.MainLoop() app.Destroy() + Index: gluon/contrib/pyfpdf/fpdf.py ================================================================== --- gluon/contrib/pyfpdf/fpdf.py +++ gluon/contrib/pyfpdf/fpdf.py @@ -1096,11 +1096,11 @@ self._out('/BitsPerComponent '+str(info['bpc'])) if 'f' in info: self._out('/Filter /'+info['f']) if 'parms' in info: self._out(info['parms']) - if('trns' in info and type([])==info['trns']): + if('trns' in info and isinstance(info['trns'],list)): trns='' for i in xrange(0,len(info['trns'])): trns+=str(info['trns'][i])+' '+str(info['trns'][i])+' ' self._out('/Mask ['+trns+']') self._out('/Length '+str(len(info['data']))+'>>') @@ -1680,7 +1680,8 @@ '\x9a':0,'\x9b':0,'\x9c':0,'\x9d':0,'\x9e':0,'\x9f':0,'\xa0':0,'\xa1':732,'\xa2':544,'\xa3':544,'\xa4':910,'\xa5':667,'\xa6':760,'\xa7':760,'\xa8':776,'\xa9':595,'\xaa':694,'\xab':626,'\xac':788,'\xad':788,'\xae':788,'\xaf':788, '\xb0':788,'\xb1':788,'\xb2':788,'\xb3':788,'\xb4':788,'\xb5':788,'\xb6':788,'\xb7':788,'\xb8':788,'\xb9':788,'\xba':788,'\xbb':788,'\xbc':788,'\xbd':788,'\xbe':788,'\xbf':788,'\xc0':788,'\xc1':788,'\xc2':788,'\xc3':788,'\xc4':788,'\xc5':788, '\xc6':788,'\xc7':788,'\xc8':788,'\xc9':788,'\xca':788,'\xcb':788,'\xcc':788,'\xcd':788,'\xce':788,'\xcf':788,'\xd0':788,'\xd1':788,'\xd2':788,'\xd3':788,'\xd4':894,'\xd5':838,'\xd6':1016,'\xd7':458,'\xd8':748,'\xd9':924,'\xda':748,'\xdb':918, '\xdc':927,'\xdd':928,'\xde':928,'\xdf':834,'\xe0':873,'\xe1':828,'\xe2':924,'\xe3':924,'\xe4':917,'\xe5':930,'\xe6':931,'\xe7':463,'\xe8':883,'\xe9':836,'\xea':836,'\xeb':867,'\xec':867,'\xed':696,'\xee':696,'\xef':874,'\xf0':0,'\xf1':874, '\xf2':760,'\xf3':946,'\xf4':771,'\xf5':865,'\xf6':771,'\xf7':888,'\xf8':967,'\xf9':888,'\xfa':831,'\xfb':873,'\xfc':927,'\xfd':970,'\xfe':918,'\xff':0} + Index: gluon/contrib/pyfpdf/html.py ================================================================== --- gluon/contrib/pyfpdf/html.py +++ gluon/contrib/pyfpdf/html.py @@ -24,11 +24,11 @@ return r, g, b class HTML2FPDF(HTMLParser): "Render basic HTML to FPDF" - def __init__(self, pdf, image_map): + def __init__(self, pdf, image_map, **kwargs): HTMLParser.__init__(self) self.image_map = image_map self.style = {} self.pre = False self.href = '' @@ -37,11 +37,13 @@ self.font_list = ("times","courier", "helvetica") self.pdf = pdf self.r = self.g = self.b = 0 self.indent = 0 self.bullet = [] - self.set_font("times", 12) + self.font_face="times" # initialize font + self.color=0 # initialize font color + self.set_font(kwargs.get("font","times"), kwargs.get("fontsize",12)) self.table = None # table attributes self.table_col_width = None # column (header) widths self.table_col_index = None # current column index self.td = None # cell attributes self.th = False # header enabled @@ -213,12 +215,12 @@ if 'face' in attrs and attrs['face'].lower() in self.font_list: face = attrs.get('face').lower() self.pdf.set_font(face) self.font_face = face if 'size' in attrs: - face = attrs.get('size') - self.pdf.set_font('', size) + size = int(attrs.get('size')) + self.pdf.set_font(self.font_face, size=int(size)) self.font_size = size if tag=='table': self.table = dict([(k.lower(), v) for k,v in attrs.items()]) if not 'width' in self.table: self.table['width'] = '100%' @@ -323,13 +325,13 @@ self.th = False if tag=='font': if self.color: self.pdf.set_text_color(0,0,0) self.color = None - if self.font: - self.SetFont('Times','',12) - self.font = None + if self.font_face: + self.set_font('Times',12) + if tag=='center': self.align = None def set_font(self, face=None, size=None): if face: @@ -378,13 +380,13 @@ self.pdf.ln(2) self.pdf.line(self.pdf.get_x(),self.pdf.get_y(),self.pdf.get_x()+187,self.pdf.get_y()) self.pdf.ln(3) class HTMLMixin(): - def write_html(self, text, image_map=lambda x:x): + def write_html(self, text, image_map=lambda x:x, **kwargs): "Parse HTML and convert it to PDF" - h2p = HTML2FPDF(self,image_map=image_map) + h2p = HTML2FPDF(self,image_map=image_map,**kwargs) h2p.feed(text) if __name__=='__main__': html="""

    html2fpdf

    @@ -451,6 +453,7 @@ pdf.write_html(html) pdf.output('html.pdf','F') import os os.system("evince html.pdf") + Index: gluon/contrib/pyfpdf/template.py ================================================================== --- gluon/contrib/pyfpdf/template.py +++ gluon/contrib/pyfpdf/template.py @@ -272,6 +272,7 @@ f.render("./invoice.pdf") if sys.platform.startswith("linux"): os.system("evince ./invoice.pdf") else: os.system("./invoice.pdf") + Index: gluon/contrib/pymysql/__init__.py ================================================================== --- gluon/contrib/pymysql/__init__.py +++ gluon/contrib/pymysql/__init__.py @@ -108,11 +108,11 @@ return True # match MySQLdb.thread_safe() def install_as_MySQLdb(): """ After this function is called, any application that imports MySQLdb or - _mysql will unwittingly actually use + _mysql will unwittingly actually use """ sys.modules["MySQLdb"] = sys.modules["_mysql"] = sys.modules["pymysql"] __all__ = [ 'BINARY', 'Binary', 'Connect', 'Connection', 'DATE', 'Date', @@ -127,5 +127,6 @@ "install_as_MySQLdb", "NULL","__version__", ] + Index: gluon/contrib/pymysql/charset.py ================================================================== --- gluon/contrib/pymysql/charset.py +++ gluon/contrib/pymysql/charset.py @@ -169,6 +169,7 @@ def charset_by_name(name): return _charsets.by_name(name) def charset_by_id(id): return _charsets.by_id(id) + Index: gluon/contrib/pymysql/connections.py ================================================================== --- gluon/contrib/pymysql/connections.py +++ gluon/contrib/pymysql/connections.py @@ -52,11 +52,11 @@ DEFAULT_CHARSET = 'latin1' MAX_PACKET_LENGTH = 256*256*256-1 def dump_packet(data): - + def is_ascii(data): if byte2int(data) >= 65 and byte2int(data) <= 122: #data.isalnum(): return data return '.' print "packet length %d" % len(data) @@ -713,11 +713,11 @@ # def _execute_command(self, command, sql): self._send_command(command, sql) - + def _request_authentication(self): self._send_authentication() def _send_authentication(self): sock = self.socket @@ -928,5 +928,6 @@ description.append(field.description()) eof_packet = self.connection.read_packet() assert eof_packet.is_eof_packet(), 'Protocol error, expecting EOF' self.description = tuple(description) + Index: gluon/contrib/pymysql/converters.py ================================================================== --- gluon/contrib/pymysql/converters.py +++ gluon/contrib/pymysql/converters.py @@ -342,5 +342,6 @@ return unicode(obj) encoders[Decimal] = escape_decimal except ImportError: pass + Index: gluon/contrib/pymysql/cursors.py ================================================================== --- gluon/contrib/pymysql/cursors.py +++ gluon/contrib/pymysql/cursors.py @@ -246,5 +246,6 @@ OperationalError = OperationalError IntegrityError = IntegrityError InternalError = InternalError ProgrammingError = ProgrammingError NotSupportedError = NotSupportedError + Index: gluon/contrib/pymysql/err.py ================================================================== --- gluon/contrib/pymysql/err.py +++ gluon/contrib/pymysql/err.py @@ -9,15 +9,15 @@ except ImportError: import sys e = sys.modules['exceptions'] Exception = e.Exception Warning = e.Warning - + from constants import ER class MySQLError(Exception): - + """Exception related to operation with MySQL.""" class Warning(Warning, MySQLError): @@ -107,11 +107,11 @@ _map_error(NotSupportedError, ER.WARNING_NOT_COMPLETE_ROLLBACK, ER.NOT_SUPPORTED_YET, ER.FEATURE_DISABLED, ER.UNKNOWN_STORAGE_ENGINE) del _map_error, ER - + def _get_error_info(data): errno = struct.unpack(' -<%(soap_ns)s:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" + self.__xml = """ +<%(soap_ns)s:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:%(soap_ns)s="%(soap_uri)s"> <%(soap_ns)s:Body> <%(method)s xmlns="%(namespace)s"> @@ -102,13 +102,13 @@ "Return a pseudo-method that can be called" if not self.services: # not using WSDL? return lambda self=self, *args, **kwargs: self.call(attr,*args,**kwargs) else: # using WSDL: return lambda self=self, *args, **kwargs: self.wsdl_call(attr,*args,**kwargs) - + def call(self, method, *args, **kwargs): - "Prepare xml request and make SOAP call, returning a SimpleXMLElement" + "Prepare xml request and make SOAP call, returning a SimpleXMLElement" #TODO: method != input_message # Basic SOAP request: xml = self.__xml % dict(method=method, namespace=self.namespace, ns=self.__ns, soap_ns=self.__soap_ns, soap_uri=soap_namespaces[self.__soap_ns]) request = SimpleXMLElement(xml,namespace=self.__ns and self.namespace, prefix=self.__ns) @@ -129,17 +129,17 @@ self.xml_response = self.send(method, self.xml_request) response = SimpleXMLElement(self.xml_response, namespace=self.namespace) if self.exceptions and response("Fault", ns=soap_namespaces.values(), error=False): raise SoapFault(unicode(response.faultcode), unicode(response.faultstring)) return response - + def send(self, method, xml): "Send SOAP request using HTTP" if self.location == 'test': return location = "%s" % self.location #?op=%s" % (self.location, method) if self.services: - soap_action = self.action + soap_action = self.action else: soap_action = self.action+method headers={ 'Content-type': 'text/xml; charset="UTF-8"', 'Content-length': str(len(xml)), @@ -152,12 +152,12 @@ print u"\n%s" % xml.decode("utf8","ignore") response, content = self.http.request( location,"POST", body=xml, headers=headers ) self.response = response self.content = content - if self.trace: - print + if self.trace: + print print '\n'.join(["%s: %s" % (k,v) for k,v in response.items()]) print content#.decode("utf8","ignore") print "="*80 return content @@ -180,11 +180,11 @@ if not operation: raise RuntimeError("Operation %s not found in WSDL: " "Service/Port Type: %s" % (method, self.service_port)) return operation - + def wsdl_call(self, method, *args, **kwargs): "Pre and post process SOAP call, input and output parameters using WSDL" soap_uri = soap_namespaces[self.__soap_ns] operation = self.get_operation(method) # get i/o type declarations: @@ -200,21 +200,21 @@ v = d.get(k) if v: if isinstance(v, dict): v = sort_dict(od[k], v) elif isinstance(v, list): - v = [sort_dict(od[k][0], v1) + v = [sort_dict(od[k][0], v1) for v1 in v] - ret[str(k)] = v + ret[str(k)] = v return ret else: return d if input and kwargs: params = sort_dict(input.values()[0], kwargs).items() method = input.keys()[0] #elif not input: - #TODO: no message! (see wsmtxca.dummy) + #TODO: no message! (see wsmtxca.dummy) else: params = kwargs and kwargs.items() # call remote procedure response = self.call(method, *params) # parse results: @@ -224,14 +224,14 @@ def help(self, method): "Return operation documentation and invocation/returned value example" operation = self.get_operation(method) input = operation['input'].values() input = input and input[0] - output = operation['output'].values()[0] + output = operation['output'].values()[0] return u"%s(%s)\n -> %s:\n\n%s" % ( - method, - input and ", ".join("%s=%s" % (k,repr(v)) for k,v + method, + input and ", ".join("%s=%s" % (k,repr(v)) for k,v in input.items()) or "", output and output or "", operation.get("documentation",""), ) @@ -242,22 +242,22 @@ "http://schemas.xmlsoap.org/wsdl/soap12/": 'soap12', } wsdl_uri="http://schemas.xmlsoap.org/wsdl/" xsd_uri="http://www.w3.org/2001/XMLSchema" xsi_uri="http://www.w3.org/2001/XMLSchema-instance" - + get_local_name = lambda s: str((':' in s) and s.split(':')[1] or s) - + REVERSE_TYPE_MAP = dict([(v,k) for k,v in TYPE_MAP.items()]) def fetch(url): "Fetch a document from a URL, save it locally if cache enabled" import os, hashlib - # make md5 hash of the url for caching... + # make md5 hash of the url for caching... filename = "%s.xml" % hashlib.md5(url).hexdigest() if isinstance(cache, basestring): - filename = os.path.join(cache, filename) + filename = os.path.join(cache, filename) if cache and os.path.exists(filename): if debug: print "Reading file %s" % (filename, ) f = open(filename, "r") xml = f.read() f.close() @@ -269,11 +269,11 @@ if debug: print "Writing file %s" % (filename, ) f = open(filename, "w") f.write(xml) f.close() return xml - + # Open uri and read xml: xml = fetch(url) # Parse WSDL XML: wsdl = SimpleXMLElement(xml, namespace=wsdl_uri) @@ -287,18 +287,18 @@ xsd_ns = get_local_name(k) # Extract useful data: self.namespace = wsdl['targetNamespace'] self.documentation = unicode(wsdl('documentation', error=False) or '') - + services = {} bindings = {} # binding_name: binding operations = {} # operation_name: operation port_type_bindings = {} # port_type_name: binding messages = {} # message: element elements = {} # element: type def - + for service in wsdl.service: service_name=service['name'] if not service_name: continue # empty service? if debug: print "Processing service", service_name @@ -313,11 +313,11 @@ bindings[binding_name] = {'service_name': service_name, 'location': location, 'soap_uri': soap_uri, 'soap_ver': soap_ver, } serv['ports'][port['name']] = bindings[binding_name] - + for binding in wsdl.binding: binding_name = binding['name'] if debug: print "Processing binding", service_name soap_binding = binding('binding', ns=soap_uris.values(), error=False) transport = soap_binding and soap_binding['transport'] or None @@ -335,11 +335,11 @@ bindings[binding_name]['operations'][op_name] = d d.update({'name': op_name}) #if action: #TODO: separe operation_binding from operation if action: d["action"] = action - + #TODO: cleanup element/schema/types parsing: def process_element(element_name, node): "Parse and define simple element types" if debug: print "Processing element", element_name for tag in node: @@ -353,11 +353,11 @@ children = tag.children() alias = False else: if debug: print element_name,"has not children!",tag continue #TODO: abstract? - d = OrderedDict() + d = OrderedDict() for e in children: t = e['type'] if not t: t = e['base'] # complexContent (extension)! if not t: @@ -389,11 +389,11 @@ # extend base element: process_element(element_name, e.children()) elements.setdefault(element_name, OrderedDict()).update(d) # check axis2 namespace at schema types attributes - self.namespace = dict(wsdl.types("schema", ns=xsd_uri)[:]).get('targetNamespace', self.namespace) + self.namespace = dict(wsdl.types("schema", ns=xsd_uri)[:]).get('targetNamespace', self.namespace) imported_schemas = {} def preprocess_schema(schema): "Find schema elements and complex types" @@ -453,14 +453,14 @@ #break if isinstance(v, list): for n in v: # recurse list postprocess_element(n) - + # process current wsdl schema: - for schema in wsdl.types("schema", ns=xsd_uri): - preprocess_schema(schema) + for schema in wsdl.types("schema", ns=xsd_uri): + preprocess_schema(schema) postprocess_element(elements) for message in wsdl.message: if debug: print "Processing message", message['name'] @@ -471,38 +471,38 @@ if not element_name: element_name = part['type'] # some uses type instead element_name = get_local_name(element_name) element = {element_name: elements.get(element_name)} messages[message['name']] = element - + for port_type in wsdl.portType: port_type_name = port_type['name'] if debug: print "Processing port type", port_type_name binding = port_type_bindings[port_type_name] for operation in port_type.operation: op_name = operation['name'] - op = operations[op_name] + op = operations[op_name] op['documentation'] = unicode(operation('documentation', error=False) or '') - if binding['soap_ver']: + if binding['soap_ver']: #TODO: separe operation_binding from operation (non SOAP?) input = get_local_name(operation.input['message']) output = get_local_name(operation.output['message']) op['input'] = messages[input] op['output'] = messages[output] if debug: import pprint pprint.pprint(services) - + return services def parse_proxy(proxy_str): "Parses proxy address user:pass@host:port into a dict suitable for httplib2" proxy_dict = {} if proxy_str is None: - return + return if "@" in proxy_str: user_pass, host_port = proxy_str.split("@") else: user_pass, host_port = "", proxy_str if ":" in host_port: @@ -509,23 +509,23 @@ host, port = host_port.split(":") proxy_dict['proxy_host'], proxy_dict['proxy_port'] = host, int(port) if ":" in user_pass: proxy_dict['proxy_user'], proxy_dict['proxy_pass'] = user_pass.split(":") return proxy_dict - - + + if __name__=="__main__": import sys - + if '--web2py' in sys.argv: # test local sample webservice exposed by web2py from client import SoapClient if not '--wsdl' in sys.argv: client = SoapClient( location = "http://127.0.0.1:8000/webservices/sample/call/soap", action = 'http://127.0.0.1:8000/webservices/sample/call/soap', # SOAPAction - namespace = "http://127.0.0.1:8000/webservices/sample/call/soap", + namespace = "http://127.0.0.1:8000/webservices/sample/call/soap", soap_ns='soap', trace = True, ns = False, exceptions=True) else: client = SoapClient(wsdl="http://127.0.0.1:8000/webservices/sample/call/soap?WSDL",trace=True) response = client.Dummy() print 'dummy', response @@ -543,17 +543,17 @@ # raw (unmarshalled parameter) local sample webservice exposed by web2py from client import SoapClient client = SoapClient( location = "http://127.0.0.1:8000/webservices/sample/call/soap", action = 'http://127.0.0.1:8000/webservices/sample/call/soap', # SOAPAction - namespace = "http://127.0.0.1:8000/webservices/sample/call/soap", + namespace = "http://127.0.0.1:8000/webservices/sample/call/soap", soap_ns='soap', trace = True, ns = False) params = SimpleXMLElement("""32""") # manully convert returned type response = client.call('AddIntegers',params) - result = response.AddResult + result = response.AddResult print int(result) # manully convert returned type - + if '--ctg' in sys.argv: # test AFIP Agriculture webservice client = SoapClient( location = "https://fwshomo.afip.gov.ar/wsctg/services/CTGService", action = 'http://impl.service.wsctg.afip.gov.ar/CTGService/', # SOAPAction @@ -563,11 +563,11 @@ response = client.dummy() result = response.dummyResponse print str(result.appserver) print str(result.dbserver) print str(result.authserver) - + if '--wsfe' in sys.argv: # Demo & Test (AFIP Electronic Invoice): ta_file = open("TA.xml") try: ta_string = ta_file.read() # read access ticket (wsaa.py) @@ -590,11 +590,11 @@ if int(results.FERecuperaQTYRequestResult.RError.percode) != 0: print "Percode: %s" % results.FERecuperaQTYRequestResult.RError.percode print "MSGerror: %s" % results.FERecuperaQTYRequestResult.RError.perrmsg else: print int(results.FERecuperaQTYRequestResult.qty.value) - + if '--feriados' in sys.argv: # Demo & Test: Argentina Holidays (Ministerio del Interior): # this webservice seems disabled from datetime import datetime, timedelta client = SoapClient( @@ -635,15 +635,15 @@ ta = SimpleXMLElement(ta_string) token = str(ta.credentials.token) sign = str(ta.credentials.sign) response = client.FEXGetCMP( Auth={"Token": token, "Sign": sign, "Cuit": 20267565393}, - Cmp={"Tipo_cbte": 19, "Punto_vta": 1, "Cbte_nro": 1}) + Cmp={"Tipo_cbte": 19, "Punto_vta": 1, "Cbte_nro": 1}) result = response['FEXGetCMPResult'] if False: print result if 'FEXErr' in result: - print "FEXError:", result['FEXErr']['ErrCode'], result['FEXErr']['ErrCode'] + print "FEXError:", result['FEXErr']['ErrCode'], result['FEXErr']['ErrCode'] cbt = result['FEXResultGet'] print cbt['Cae'] FEX_event = result['FEXEvents'] print FEX_event['EventCode'], FEX_event['EventMsg'] @@ -667,22 +667,23 @@ response = client.obtenerProvincias(auth={"token":token, "sign":sign, "cuitRepresentado":20267565393}) print "response=",response for ret in response: print ret['return']['codigoProvincia'], ret['return']['descripcionProvincia'].encode("latin1") prueba = dict(numeroCartaDePorte=512345678, codigoEspecie=23, - cuitRemitenteComercial=20267565393, cuitDestino=20267565393, cuitDestinatario=20267565393, - codigoLocalidadOrigen=3058, codigoLocalidadDestino=3059, - codigoCosecha='0910', pesoNetoCarga=1000, cantHoras=1, + cuitRemitenteComercial=20267565393, cuitDestino=20267565393, cuitDestinatario=20267565393, + codigoLocalidadOrigen=3058, codigoLocalidadDestino=3059, + codigoCosecha='0910', pesoNetoCarga=1000, cantHoras=1, patenteVehiculo='CZO985', cuitTransportista=20267565393, numeroCTG="43816783", transaccion='10000001681', observaciones='', ) - response = client.solicitarCTG( + response = client.solicitarCTG( auth={"token": token, "sign": sign, "cuitRepresentado": 20267565393}, solicitarCTGRequest= prueba) print response['return']['numeroCTG'] ##print parse_proxy(None) ##print parse_proxy("host:1234") ##print parse_proxy("user:pass@host:1234") - ##sys.exit(0) + ##sys.exit(0) + Index: gluon/contrib/pysimplesoap/server.py ================================================================== --- gluon/contrib/pysimplesoap/server.py +++ gluon/contrib/pysimplesoap/server.py @@ -22,14 +22,14 @@ DEBUG = False class SoapDispatcher(object): "Simple Dispatcher for SOAP Server" - - def __init__(self, name, documentation='', action='', location='', - namespace=None, prefix=False, - soap_uri="http://schemas.xmlsoap.org/soap/envelope/", + + def __init__(self, name, documentation='', action='', location='', + namespace=None, prefix=False, + soap_uri="http://schemas.xmlsoap.org/soap/envelope/", soap_ns='soap', **kwargs): self.methods = {} self.name = name self.documentation = documentation @@ -37,14 +37,14 @@ self.location = location self.namespace = namespace # targetNamespace self.prefix = prefix self.soap_ns = soap_ns self.soap_uri = soap_uri - + def register_function(self, name, fn, returns=None, args=None, doc=None): self.methods[name] = fn, returns, args, doc or getattr(fn,"__doc__","") - + def dispatch(self, xml, action=None): "Receive and proccess SOAP call" # default values: prefix = self.prefix ret = fault = None @@ -58,66 +58,66 @@ for k, v in request[:]: if v in ("http://schemas.xmlsoap.org/soap/envelope/", "http://www.w3.org/2003/05/soap-env",): soap_ns = request.attributes()[k].localName soap_uri = request.attributes()[k].value - + soap_fault_code = 'Client' - - # parse request message and get local method + + # parse request message and get local method method = request('Body', ns=soap_uri).children()(0) if action: - # method name = action + # method name = action name = action[len(self.action)+1:-1] prefix = self.prefix if not action or not name: # method name = input message name name = method.get_local_name() prefix = method.get_prefix() if DEBUG: print "dispatch method", name function, returns_types, args_types, doc = self.methods[name] - + # de-serialize parameters (if type definitions given) if args_types: args = method.children().unmarshall(args_types) elif args_types is None: args = {'request':method} # send raw request else: args = {} # no parameters - + soap_fault_code = 'Server' # execute function ret = function(**args) if DEBUG: print ret except Exception, e: import sys etype, evalue, etb = sys.exc_info() - if DEBUG: + if DEBUG: import traceback detail = ''.join(traceback.format_exception(etype, evalue, etb)) detail += '\n\nXML REQUEST\n\n' + xml else: detail = None - fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), - 'faultstring': unicode(evalue), + fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), + 'faultstring': unicode(evalue), 'detail': detail} # build response message if not prefix: - xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>""" + xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>""" else: xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" - xmlns:%(prefix)s="%(namespace)s"/>""" - + xmlns:%(prefix)s="%(namespace)s"/>""" + xml = xml % {'namespace': self.namespace, 'prefix': prefix, 'soap_ns': soap_ns, 'soap_uri': soap_uri} response = SimpleXMLElement(xml, namespace=self.namespace, prefix=prefix) - + response['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" response['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" body = response.add_child("%s:Body" % soap_ns, ns=False) if fault: @@ -144,11 +144,11 @@ # Introspection functions: def list_methods(self): "Return a list of aregistered operations" - return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()] + return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()] def help(self, method=None): "Generate sample request and response messages" (function, returns, args, doc) = self.methods[method] xml = """ @@ -183,11 +183,11 @@ def wsdl(self): "Generate Web Service Description v1.1" xml = """ - @@ -204,11 +204,11 @@ """ % {'namespace': self.namespace, 'name': self.name, 'documentation': self.documentation} wsdl = SimpleXMLElement(xml) for method, (function, returns, args, doc) in self.methods.items(): # create elements: - + def parse_element(name, values, array=False, complex=False): if not complex: element = wsdl('wsdl:types')('xsd:schema').add_child('xsd:element') complex = element.add_child("xsd:complexType") else: @@ -239,25 +239,25 @@ l = [] for d in v: l.extend(d.items()) parse_element(n, l, array=True, complex=True) t = "tns:%s" % n - elif isinstance(v, dict): + elif isinstance(v, dict): n="%s%s" % (name, k) parse_element(n, v.items(), complex=True) t = "tns:%s" % n e.add_attribute('type', t) - + parse_element("%s" % method, args and args.items()) parse_element("%sResponse" % method, returns and returns.items()) # create messages: for m,e in ('Input',''), ('Output','Response'): message = wsdl.add_child('wsdl:message') message['name'] = "%s%s" % (method, m) part = message.add_child("wsdl:part") - part[:] = {'name': 'parameters', + part[:] = {'name': 'parameters', 'element': 'tns:%s%s' % (method,e)} # create ports portType = wsdl.add_child('wsdl:portType') portType['name'] = "%sPortType" % self.name @@ -300,11 +300,11 @@ port["name"] = "%s" % self.name port["binding"] = "tns:%sBinding" % self.name soapaddress = port.add_child('soap:address') soapaddress["location"] = self.location return wsdl.as_xml(pretty=True) - + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer class SOAPHandler(BaseHTTPRequestHandler): def do_GET(self): "User viewable help information and wsdl" @@ -320,11 +320,11 @@ # return supplied method help (?request or ?response messages) req, res, doc = self.server.dispatcher.help(args[0]) if len(args)==1 or args[1]=="request": response = req else: - response = res + response = res self.send_response(200) self.send_header("Content-type", "text/xml") self.end_headers() self.wfile.write(response) @@ -347,11 +347,11 @@ action = 'http://localhost:8008/', # SOAPAction namespace = "http://example.com/pysimplesoapsamle/", prefix="ns0", documentation = 'Example soap service using PySimpleSoap', trace = True, ns = True) - + def adder(p,c, dt=None): "Add several values" print c[0]['d'],c[1]['d'], import datetime dt = dt + datetime.timedelta(365) @@ -364,15 +364,15 @@ def echo(request): "Copy request->response (generic, any type)" return request.value dispatcher.register_function('Adder', adder, - returns={'AddResult': {'ab': int, 'dd': str } }, + returns={'AddResult': {'ab': int, 'dd': str } }, args={'p': {'a': int,'b': int}, 'dt': Date, 'c': [{'d': Decimal}]}) dispatcher.register_function('Dummy', dummy, - returns={'out0': str}, + returns={'out0': str}, args={'in0': str}) dispatcher.register_function('Echo', echo) if '--local' in sys.argv: @@ -383,11 +383,11 @@ try: testfile.write(wsdl) finally: testfile.close() # dummy local test (clasic soap dialect) - xml = """ + xml = """

    12

    5000000.1.2
    20100724
    @@ -410,11 +410,11 @@ """ print dispatcher.dispatch(xml) # echo local test (generic soap service) - xml = """ + xml = """ @@ -428,11 +428,11 @@ for method, doc in dispatcher.list_methods(): request, response, doc = dispatcher.help(method) ##print request ##print response - + if '--serve' in sys.argv: print "Starting server..." httpd = HTTPServer(("", 8008), SOAPHandler) httpd.dispatcher = dispatcher httpd.serve_forever() @@ -440,15 +440,16 @@ if '--consume' in sys.argv: from client import SoapClient client = SoapClient( location = "http://localhost:8008/", action = 'http://localhost:8008/', # SOAPAction - namespace = "http://example.com/sample.wsdl", + namespace = "http://example.com/sample.wsdl", soap_ns='soap', trace = True, ns = False) response = client.Adder(p={'a':1,'b':2},dt='20100724',c=[{'d':'1.20'},{'d':'2.01'}]) result = response.AddResult print int(result.ab) print str(result.dd) + Index: gluon/contrib/pysimplesoap/simplexml.py ================================================================== --- gluon/contrib/pysimplesoap/simplexml.py +++ gluon/contrib/pysimplesoap/simplexml.py @@ -17,11 +17,11 @@ __license__ = "LGPL 3.0" __version__ = "1.02c" import xml.dom.minidom from decimal import Decimal -import datetime +import datetime import time DEBUG = False # Functions to serialize/unserialize special immutable types: @@ -39,11 +39,11 @@ self.py_type, self.xml_type = py_type, xml_type def __call__(self, value): return self.py_type(value) def __repr__(self): return "" % (self.xml_type, self.py_type) - + byte = Alias(str,'byte') short = Alias(int,'short') double = Alias(float,'double') integer = Alias(long,'integer') DateTime = datetime.datetime @@ -51,11 +51,11 @@ Time = datetime.time # Define convertion function (python type): xml schema type TYPE_MAP = {str:'string',unicode:'string', bool:'boolean', short:'short', byte:'byte', - int:'int', long:'long', integer:'integer', + int:'int', long:'long', integer:'integer', float:'float', double:'double', Decimal:'decimal', datetime.datetime:'dateTime', datetime.date:'date', } TYPE_MARSHAL_FN = {datetime.datetime:datetime_m, datetime.date:date_m,} @@ -101,11 +101,11 @@ return s class SimpleXMLElement(object): "Simple XML manipulation (simil PHP)" - + def __init__(self, text = None, elements = None, document = None, namespace = None, prefix=None): self.__ns = namespace self.__prefix = prefix if text: try: @@ -115,11 +115,11 @@ raise self.__elements = [self.__document.documentElement] else: self.__elements = elements self.__document = document - + def add_child(self,name,text=None,ns=True): "Adding a child tag to a node" if not ns or not self.__ns: if DEBUG: print "adding %s" % (name) element = self.__document.createElement(name) @@ -138,11 +138,11 @@ return SimpleXMLElement( elements=[element], document=self.__document, namespace=self.__ns, prefix=self.__prefix) - + def __setattr__(self, tag, text): "Add text child tag node (short form)" if tag.startswith("_"): object.__setattr__(self, tag, text) else: @@ -187,11 +187,11 @@ #TODO: use slice syntax [:]? return self._element.attributes def __getitem__(self, item): "Return xml tag attribute value or a slice of attributes (iter)" - if DEBUG: print "__getitem__(%s)" % item + if DEBUG: print "__getitem__(%s)" % item if isinstance(item,basestring): if self._element.hasAttribute(item): return self._element.attributes[item].value elif isinstance(item, slice): # return a list with name:values @@ -202,15 +202,15 @@ return SimpleXMLElement( elements=[element], document=self.__document, namespace=self.__ns, prefix=self.__prefix) - + def add_attribute(self, name, value): "Set an attribute value from a string" self._element.setAttribute(name, value) - + def __setitem__(self, item, value): "Set an attribute value" if isinstance(item,basestring): self.add_attribute(item, value) elif isinstance(item, slice): @@ -233,11 +233,11 @@ elements=[self.__elements[tag]] if ns and not elements: for ns_uri in isinstance(ns, (tuple, list)) and ns or (ns, ): if DEBUG: print "searching %s by ns=%s" % (tag,ns_uri) elements = self._element.getElementsByTagNameNS(ns_uri, tag) - if elements: + if elements: break if self.__ns and not elements: if DEBUG: print "searching %s by ns=%s" % (tag, self.__ns) elements = self._element.getElementsByTagNameNS(self.__ns, tag) if not elements: @@ -258,11 +258,11 @@ raise AttributeError("Tag not found: %s (%s)" % (tag, str(e))) def __getattr__(self, tag): "Shortcut for __call__" return self.__call__(tag) - + def __iter__(self): "Iterate over xml tags at this level" try: for __element in self.__elements: yield SimpleXMLElement( @@ -273,11 +273,11 @@ except: raise def __dir__(self): "List xml children tags names" - return [node.tagName for node + return [node.tagName for node in self._element.childNodes if node.nodeType != node.TEXT_NODE] def children(self): "Return xml children tags element" @@ -293,25 +293,25 @@ prefix=self.__prefix) def __len__(self): "Return elements count" return len(self.__elements) - + def __contains__( self, item): "Search for a tag name in this element or child nodes" return self._element.getElementsByTagName(item) - + def __unicode__(self): "Returns the unicode text nodes of the current element" if self._element.childNodes: rc = u"" for node in self._element.childNodes: if node.nodeType == node.TEXT_NODE: rc = rc + node.data return rc return '' - + def __str__(self): "Returns the str text nodes of the current element" return unicode(self).encode("utf8","ignore") def __int__(self): @@ -321,12 +321,12 @@ def __float__(self): "Returns the float value of the current element" try: return float(self.__str__()) except: - raise IndexError(self._element.toxml()) - + raise IndexError(self._element.toxml()) + _element = property(lambda self: self.__elements[0]) def unmarshall(self, types): "Convert to python values the current serialized xml element" # types is a dict of {tag name: convertion function} @@ -352,11 +352,11 @@ if fn is None: # xsd:anyType not unmarshalled value = node elif str(node) or fn == str: try: # get special desserialization function (if any) - fn = TYPE_UNMARSHAL_FN.get(fn,fn) + fn = TYPE_UNMARSHAL_FN.get(fn,fn) value = fn(unicode(node)) except (ValueError, TypeError), e: raise ValueError("Tag: %s: %s" % (name, unicode(e))) else: value = None @@ -383,16 +383,16 @@ self.add_child(name,value,ns=ns) elif value is None: # sent a empty tag? self.add_child(name,ns=ns) elif value in TYPE_MAP.keys(): # add commented placeholders for simple tipes (for examples/help only) - child = self.add_child(name,ns=ns) + child = self.add_child(name,ns=ns) child.add_comment(TYPE_MAP[value]) - else: # the rest of object types are converted to string + else: # the rest of object types are converted to string # get special serialization function (if any) fn = TYPE_MARSHAL_FN.get(type(value),str) - self.add_child(name,fn(value),ns=ns) + self.add_child(name,fn(value),ns=ns) def import_node(self, other): x = self.__document.importNode(other._element, True) # deep copy self._element.appendChild(x) @@ -404,12 +404,13 @@ assert int(span.prueba.i)==1 and float(span.prueba.float)==1.5 span1 = SimpleXMLElement('googleyahoohotmail') assert [str(a) for a in span1.a()] == ['google', 'yahoo', 'hotmail'] span1.add_child('a','altavista') span1.b = "ex msn" - d = {'href':'http://www.bing.com/', 'alt': 'Bing'} + d = {'href':'http://www.bing.com/', 'alt': 'Bing'} span1.b[:] = d assert sorted([(k,v) for k,v in span1.b[:]]) == sorted(d.items()) print span1.as_xml() assert 'b' in span1 span.import_node(span1) print span.as_xml() + Index: gluon/contrib/rss2.py ================================================================== --- gluon/contrib/rss2.py +++ gluon/contrib/rss2.py @@ -584,6 +584,7 @@ return dumps(rss) if __name__ == '__main__': print test() + Index: gluon/contrib/shell.py ================================================================== --- gluon/contrib/shell.py +++ gluon/contrib/shell.py @@ -262,6 +262,7 @@ return output.getvalue() if __name__=='__main__': history=History() while True: print run(history, raw_input('>>> ')).rstrip() + Index: gluon/contrib/simplejson/__init__.py ================================================================== --- gluon/contrib/simplejson/__init__.py +++ gluon/contrib/simplejson/__init__.py @@ -409,11 +409,11 @@ import scanner as scan c_make_encoder = _import_c_make_encoder() if enabled: dec.scanstring = dec.c_scanstring or dec.py_scanstring enc.c_make_encoder = c_make_encoder - enc.encode_basestring_ascii = (enc.c_encode_basestring_ascii or + enc.encode_basestring_ascii = (enc.c_encode_basestring_ascii or enc.py_encode_basestring_ascii) scan.make_scanner = scan.c_make_scanner or scan.py_make_scanner else: dec.scanstring = dec.py_scanstring enc.c_make_encoder = None @@ -435,5 +435,6 @@ indent=None, separators=None, encoding='utf-8', default=None, ) + Index: gluon/contrib/simplejson/decoder.py ================================================================== --- gluon/contrib/simplejson/decoder.py +++ gluon/contrib/simplejson/decoder.py @@ -418,5 +418,6 @@ try: obj, end = self.scan_once(s, idx) except StopIteration: raise JSONDecodeError("No JSON object could be decoded", s, idx) return obj, end + Index: gluon/contrib/simplejson/encoder.py ================================================================== --- gluon/contrib/simplejson/encoder.py +++ gluon/contrib/simplejson/encoder.py @@ -498,5 +498,6 @@ yield chunk if markers is not None: del markers[markerid] return _iterencode + Index: gluon/contrib/simplejson/ordered_dict.py ================================================================== --- gluon/contrib/simplejson/ordered_dict.py +++ gluon/contrib/simplejson/ordered_dict.py @@ -115,5 +115,6 @@ all(p==q for p, q in zip(self.items(), other.items())) return dict.__eq__(self, other) def __ne__(self, other): return not self == other + Index: gluon/contrib/simplejson/scanner.py ================================================================== --- gluon/contrib/simplejson/scanner.py +++ gluon/contrib/simplejson/scanner.py @@ -74,5 +74,6 @@ memo.clear() return scan_once make_scanner = c_make_scanner or py_make_scanner + Index: gluon/contrib/simplejson/tool.py ================================================================== --- gluon/contrib/simplejson/tool.py +++ gluon/contrib/simplejson/tool.py @@ -38,5 +38,6 @@ infile.close() outfile.close() if __name__ == '__main__': main() + ADDED gluon/contrib/simplejsonrpc.py Index: gluon/contrib/simplejsonrpc.py ================================================================== --- gluon/contrib/simplejsonrpc.py +++ gluon/contrib/simplejsonrpc.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation; either version 3, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. + +"Pythonic simple JSON RPC Client implementation" + +__author__ = "Mariano Reingart (reingart@gmail.com)" +__copyright__ = "Copyright (C) 2011 Mariano Reingart" +__license__ = "LGPL 3.0" +__version__ = "0.04" + + +import urllib +from xmlrpclib import Transport, SafeTransport +from cStringIO import StringIO +import random +import sys +try: + import gluon.contrib.simplejson as json # try web2py json serializer +except ImportError: + try: + import json # try stdlib (py2.6) + except: + import simplejson as json # try external module + + +class JSONRPCError(RuntimeError): + "Error object for remote procedure call fail" + def __init__(self, code, message): + self.code = code + self.message = message + def __unicode__(self): + return u"%s: %s" % (self.code, self.message) + def __str__(self): + return self.__unicode__().encode("ascii","ignore") + + +class JSONDummyParser: + "json wrapper for xmlrpclib parser interfase" + def __init__(self): + self.buf = StringIO() + def feed(self, data): + self.buf.write(data) + def close(self): + return self.buf.getvalue() + + +class JSONTransportMixin: + "json wrapper for xmlrpclib transport interfase" + + def send_content(self, connection, request_body): + connection.putheader("Content-Type", "application/json") + connection.putheader("Content-Length", str(len(request_body))) + connection.endheaders() + if request_body: + connection.send(request_body) + # todo: add gzip compression + + def getparser(self): + # get parser and unmarshaller + parser = JSONDummyParser() + return parser, parser + + +class JSONTransport(JSONTransportMixin, Transport): + pass + +class JSONSafeTransport(JSONTransportMixin, SafeTransport): + pass + + +class ServerProxy(object): + "JSON RPC Simple Client Service Proxy" + + def __init__(self, uri, transport=None, encoding=None, verbose=0): + self.location = uri # server location (url) + self.trace = verbose # show debug messages + self.exceptions = True # raise errors? (JSONRPCError) + self.timeout = None + self.json_request = self.json_response = '' + + type, uri = urllib.splittype(uri) + if type not in ("http", "https"): + raise IOError, "unsupported JSON-RPC protocol" + self.__host, self.__handler = urllib.splithost(uri) + + if transport is None: + if type == "https": + transport = JSONSafeTransport() + else: + transport = JSONTransport() + self.__transport = transport + self.__encoding = encoding + self.__verbose = verbose + + def __getattr__(self, attr): + "pseudo method that can be called" + return lambda *args: self.call(attr, *args) + + def call(self, method, *args): + "JSON RPC communication (method invocation)" + + # build data sent to the service + request_id = random.randint(0, sys.maxint) + data = {'id': request_id, 'method': method, 'params': args, } + request = json.dumps(data) + + # make HTTP request (retry if connection is lost) + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + + # store plain request and response for further debugging + self.json_request = request + self.json_response = response + + # parse json data coming from service + # {'version': '1.1', 'id': id, 'result': result, 'error': None} + response = json.loads(response) + + if response['id'] != request_id: + raise JSONRPCError(0, "JSON Request ID != Response ID") + + self.error = response.get('error', {}) + if self.error and self.exceptions: + raise JSONRPCError(self.error.get('code', 0), self.error.get('message', '')) + + return response.get('result') + + +if __name__ == "__main__": + # basic tests: + location = "http://www.web2py.com.ar/webservices/sample/call/jsonrpc" + client = ServerProxy(location, verbose='--verbose' in sys.argv,) + print client.add(1, 2) + Index: gluon/contrib/sms_utils.py ================================================================== --- gluon/contrib/sms_utils.py +++ gluon/contrib/sms_utils.py @@ -108,6 +108,7 @@ if number[0]=='+1': number=number[1:] elif number[0]=='+': number=number[3:] elif number[:2]=='00': number=number[3:] number=re.sub('[^\d]','',number) return number+SMSCODES[provider] + Index: gluon/contrib/spreadsheet.py ================================================================== --- gluon/contrib/spreadsheet.py +++ gluon/contrib/spreadsheet.py @@ -258,6 +258,7 @@ s = Sheet(0, 0) s.cell('a', value="2") s.cell('b', value="=sin(a)") s.cell('c', value="=cos(a)**2+b*b") print s['c'].computed_value + Index: gluon/contrib/taskbar_widget.py ================================================================== --- gluon/contrib/taskbar_widget.py +++ gluon/contrib/taskbar_widget.py @@ -239,6 +239,7 @@ class EnumServerState: RUNNING = 0 STOPPED = 1 + Index: gluon/contrib/user_agent_parser.py ================================================================== --- gluon/contrib/user_agent_parser.py +++ gluon/contrib/user_agent_parser.py @@ -4,11 +4,11 @@ Aim is * fast * very easy to extend * reliable enough for practical purposes * and assist python web apps to detect clients. - + Taken from http://pypi.python.org/pypi/httpagentparser (MIT license) Modified my Ross Peoples for web2py to better support iPhone and iPad. """ import sys from storage import Storage @@ -57,10 +57,11 @@ result_key = "override me" order = 10 # 0 is highest look_for = "string to look for" skip_if_found = [] # strings if present stop processin can_register = False + is_mobile = False prefs = Storage() # dict(info_type = [name1, name2], ..) version_splitters = ["/", " "] _suggested_detectors = None def __init__(self): @@ -69,21 +70,26 @@ self.can_register = (self.__class__.__dict__.get('can_register', True)) def detect(self, agent, result): if agent and self.checkWords(agent): result[self.info_type] = Storage(name=self.name) + result[self.info_type].is_mobile = self.is_mobile + if not result.is_mobile: + result.is_mobile = result[self.info_type].is_mobile + version = self.getVersion(agent) if version: result[self.info_type].version = version + return True return False def checkWords(self, agent): for w in self.skip_if_found: if w in agent: return False - if self.look_for: + if self.look_for in agent: return True return False def getVersion(self, agent): # -> version string /None @@ -161,16 +167,16 @@ def getVersion(self, agent): if "Version/" in agent: return agent.split('Version/')[-1].split(' ')[0].strip() else: # Mobile Safari - return agent.split('Safari ')[-1].split(' ')[0].strip() + return agent.split('Safari ')[-1].split(' ')[0].strip() class Linux(OS): look_for = 'Linux' - prefs = Storage(browser=["Firefox"], + prefs = Storage(browser=["Firefox"], dist=["Ubuntu", "Android"], flavor=None) def getVersion(self, agent): pass @@ -232,29 +238,32 @@ vs = self.version_splitters return agent.split(self.look_for+vs[0])[-1].split(vs[1])[1].strip()[:-1] class Android(Dist): look_for = 'Android' + is_mobile = True def getVersion(self, agent): return agent.split('Android')[-1].split(';')[0].strip() class iPhone(Dist): look_for = 'iPhone' + is_mobile = True def getVersion(self, agent): version_end_chars = ['like', ';', ')'] part = agent.split('CPU OS')[-1].strip() for c in version_end_chars: if c in part: version = 'iOS ' + part.split(c)[0].strip() break return version.replace('_', '.') - + class iPad(Dist): look_for = 'iPad' + is_mobile = True def getVersion(self, agent): version_end_chars = ['like', ';', ')'] part = agent.split('CPU OS')[-1].strip() for c in version_end_chars: @@ -277,11 +286,11 @@ if "detector" in locals(): detector._suggested_detectors = detectors else: detectors = _suggested_detectors for detector in detectors: - print "detector name: ", detector.name + # print "detector name: ", detector.name if detector.detect(agent, result): prefs = detector.prefs _suggested_detectors = detector._suggested_detectors break return result @@ -289,10 +298,12 @@ class Result(Storage): def __missing__(self, k): return "" +""" +THIS VERSION OF DETECT CAUSES IndexErrors. def detect(agent): result = Result() _suggested_detectors = [] for info_type in detectorshub: @@ -303,35 +314,36 @@ _suggested_detectors = detectorshub.reorderByPrefs( detectors, detector.prefs.get(info_type)) detector._suggested_detectors = _suggested_detectors break return result - +""" def simple_detect(agent): """ - -> (os, browser) # tuple of strings + -> (os, browser, is_mobile) # tuple of strings """ result = detect(agent) os_list = [] if 'flavor' in result: os_list.append(result['flavor']['name']) if 'dist' in result: os_list.append(result['dist']['name']) if 'os' in result: os_list.append(result['os']['name']) os = os_list and " ".join(os_list) or "Unknown OS" - os_version = os_list and (result['flavor'] and result['flavor'].get( - 'version')) or (result['dist'] and result['dist'].get('version')) \ - or (result['os'] and result['os'].get('version')) or "" + os_version = os_list and ('flavor' in result and result['flavor'] and result['flavor'].get( + 'version')) or ('dist' in result and result['dist'] and result['dist'].get('version')) \ + or ('os' in result and result['os'] and result['os'].get('version')) or "" browser = 'browser' in result and result['browser']['name'] \ or 'Unknown Browser' browser_version = 'browser' in result \ and result['browser'].get('version') or "" if browser_version: browser = " ".join((browser, browser_version)) if os_version: os = " ".join((os, os_version)) - return os, browser + #is_mobile = ('dist' in result and result.dist.is_mobile) or ('os' in result and result.os.is_mobile) or False + return os, browser, result.is_mobile if __name__ == '__main__': import time import unittest @@ -363,11 +375,11 @@ {"os": {"name": "Linux"}, "browser": {"name": "Opera", "version": "9.80"}},), ("Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.5) Gecko/20060127 Netscape/8.1", ("Windows NT 5.1", "Netscape 8.1"), {'os': {'name': 'Windows', 'version': 'NT 5.1'}, 'browser': {'name': 'Netscape', 'version': '8.1'}},), ) - + class TestHAP(unittest.TestCase): def setUp(self): self.harass_repeat = 1000 self.data = data @@ -390,5 +402,19 @@ print "Time taken for single detecttion: ", \ time_taken / (len(self.data) * self.harass_repeat) unittest.main() + +class mobilize(object): + + def __init__(self, func): + self.func = func + + def __call__(self): + from gluon import current + user_agent = current.request.user_agent() + if user_agent.is_mobile: + items = current.response.view.split('.') + items.insert(-1,'mobile') + current.response.view = '.'.join(items) + return self.func() Index: gluon/custom_import.py ================================================================== --- gluon/custom_import.py +++ gluon/custom_import.py @@ -64,15 +64,19 @@ """ Many imports can be made for a single import statement. This method help the management of this aspect. """ - def __call__(self, name, globals={}, locals={}, fromlist=[], level=-1): + def __call__(self, name, globals=None, locals=None, + fromlist=None, level=-1): """ The import method itself. """ - return _STANDARD_PYTHON_IMPORTER(name, globals, locals, fromlist, + return _STANDARD_PYTHON_IMPORTER(name, + globals, + locals, + fromlist, level) def end(self): """ Needed for clean up. @@ -95,16 +99,21 @@ self._tl._modules_loaded = None def begin(self): self._tl._modules_loaded = set() - def __call__(self, name, globals={}, locals={}, fromlist=[], level=-1): + def __call__(self, name, globals=None, locals=None, + fromlist=None, level=-1): """ The import method itself. """ - call_begin_end = self._tl._modules_loaded == None + globals = globals or {} + locals = locals or {} + fromlist = fromlist or [] + + call_begin_end = self._tl._modules_loaded is None if call_begin_end: self.begin() try: self._tl.globals = globals @@ -240,15 +249,20 @@ if file_path.startswith(self.__web2py_path_os_path_sep): file_path = file_path[self.__web2py_path_os_path_sep_len:] return self.__RE_APP_DIR.match(file_path) return False - def __call__(self, name, globals={}, locals={}, fromlist=[], level=-1): + def __call__(self, name, globals=None, locals=None, + fromlist=None, level=-1): """ The import method itself. """ + globals = globals or {} + locals = locals or {} + fromlist = fromlist or [] + self.begin() #try: # if not relative and not from applications: if not name.startswith(".") and level <= 0 \ and not name.startswith("applications.") \ @@ -306,6 +320,8 @@ class _Web2pyDateTrackerImporter(_Web2pyImporter, _DateTrackerImporter): """ Like _Web2pyImporter but using a _DateTrackerImporter. """ + + ADDED gluon/custom_import.pyc Index: gluon/custom_import.pyc ================================================================== --- gluon/custom_import.pyc +++ gluon/custom_import.pyc cannot compute difference between binary files Index: gluon/dal.py ================================================================== --- gluon/dal.py +++ gluon/dal.py @@ -105,11 +105,11 @@ 'firebird_embedded://username:password@c://path' 'informix://user:password@server:3050/database' 'informixu://user:password@server:3050/database' # unicode informix 'google:datastore' # for google app engine datastore 'google:sql' # for google app engine with sql (mysql compatible) -'teradata://DSN=dsn;UID=user;PWD=pass' # experimental +'teradata://DSN=dsn;UID=user;PWD=pass' # experimental For more info: help(DAL) help(Field) """ @@ -117,12 +117,17 @@ ################################################################################### # this file orly exposes DAL and Field ################################################################################### __all__ = ['DAL', 'Field'] -MAXCHARLENGTH = 512 -INFINITY = 2**15 # not quite but reasonable default max char length + +MAXCHARLENGTH = 2**15 # not quite but reasonable default max char length +DEFAULTLENGTH = {'string':512, + 'password':512, + 'upload':512, + 'text':2**15, + 'blob':2**31} import re import sys import locale import os @@ -186,11 +191,11 @@ # internal representation of tables with field # ., tables and fields may only be [a-zA-Z0-0_] regex_dbname = re.compile('^(\w+)(\:\w+)*') -table_field = re.compile('^[\w_]+\.[\w_]+$') +table_field = re.compile('^([\w_]+)\.([\w_]+)$') regex_content = re.compile('(?P
    [\w\-]+)\.(?P[\w\-]+)\.(?P[\w\-]+)\.(?P\w+)\.\w+$') regex_cleanup_fn = re.compile('[\'"\s;]+') string_unpack=re.compile('(?0 it will try pull the connection from the pool + if the connection is not active (closed by db server) it will loop + if not self.pool_size or no active connections in pool makes a new one + """ if not self.pool_size: self.connection = f() + self.cursor = cursor and self.connection.cursor() else: uri = self.uri - sql_locker.acquire() - if not uri in ConnectionPool.pools: - ConnectionPool.pools[uri] = [] - if ConnectionPool.pools[uri]: - self.connection = ConnectionPool.pools[uri].pop() - sql_locker.release() - else: - sql_locker.release() - self.connection = f() + while True: + sql_locker.acquire() + if not uri in ConnectionPool.pools: + ConnectionPool.pools[uri] = [] + if ConnectionPool.pools[uri]: + self.connection = ConnectionPool.pools[uri].pop() + sql_locker.release() + self.cursor = cursor and self.connection.cursor() + try: + if self.cursor and self.check_active_connection: + self.execute('SELECT 1;') + break + except: + pass + else: + sql_locker.release() + self.connection = f() + self.cursor = cursor and self.connection.cursor() + break if not hasattr(thread,'instances'): thread.instances = [] thread.instances.append(self) @@ -419,11 +448,11 @@ ################################################################################### class BaseAdapter(ConnectionPool): driver = None - maxcharlength = INFINITY + maxcharlength = MAXCHARLENGTH commit_on_alter_table = False support_distributed_transaction = False uploads_in_blob = False types = { 'boolean': 'CHAR(1)', @@ -443,13 +472,19 @@ 'list:integer': 'TEXT', 'list:string': 'TEXT', 'list:reference': 'TEXT', } + def adapt(self,obj): + return "'%s'" % obj.replace("'", "''") + def integrity_error(self): return self.driver.IntegrityError + def operational_error(self): + return self.driver.OperationalError + def file_exists(self, filename): """ to be used ONLY for files that on GAE may not be on filesystem """ return os.path.exists(filename) @@ -796,12 +831,19 @@ return 'Random()' def NOT_NULL(self,default,field_type): return 'NOT NULL DEFAULT %s' % self.represent(default,field_type) + def COALESCE(self,first,second): + expressions = [self.expand(first)]+[self.expand(e) for e in second] + return 'COALESCE(%s)' % ','.join(expressions) + def COALESCE_ZERO(self,first): return 'COALESCE(%s,0)' % self.expand(first) + + def RAW(self,first): + return first def ALLOW_NULL(self): return '' def SUBSTRING(self,field,parameters): @@ -866,11 +908,11 @@ def BELONGS(self,first,second): if isinstance(second,str): return '(%s IN (%s))' % (self.expand(first),second[:-1]) elif second==[] or second==(): - return '(0)' + return '(1=0)' items =','.join(self.expand(item,first.type) for item in second) return '(%s IN (%s))' % (self.expand(first),items) def LIKE(self,first,second): return '(%s LIKE %s)' % (self.expand(first),self.expand(second,'string')) @@ -943,12 +985,14 @@ elif isinstance(expression, (Expression, Query)): if not expression.second is None: return expression.op(expression.first, expression.second) elif not expression.first is None: return expression.op(expression.first) - else: + elif not isinstance(expression.op,str): return expression.op() + else: + return '(%s)' % expression.op elif field_type: return self.represent(expression,field_type) elif isinstance(expression,(list,tuple)): return ','.join([self.represent(item,field_type) for item in expression]) else: @@ -996,10 +1040,11 @@ logfile.write('success!\n') finally: logfile.close() def _update(self,tablename,query,fields): + query = self.filter_tenant(query,[tablename]) if query: sql_w = ' WHERE ' + self.expand(query) else: sql_w = '' sql_v = ','.join(['%s=%s' % (field.name, self.expand(value,field.type)) for (field,value) in fields]) @@ -1012,10 +1057,11 @@ return self.cursor.rowcount except: return None def _delete(self,tablename, query): + query = self.filter_tenant(query,[tablename]) if query: sql_w = ' WHERE ' + self.expand(query) else: sql_w = '' return 'DELETE FROM %s%s;' % (tablename, sql_w) @@ -1191,21 +1237,22 @@ rows = self.rowslice(rows,limitby[0],None) return self.parse(rows,self._colnames) def _count(self,query,distinct=None): tablenames = self.tables(query) + query = self.filter_tenant(query,tablenames) if query: sql_w = ' WHERE ' + self.expand(query) else: sql_w = '' sql_t = ','.join(tablenames) if distinct: if isinstance(distinct,(list,tuple)): distinct = xorify(distinct) sql_d = self.expand(distinct) - return 'SELECT count(DISTINCT %s) FROM %s%s' % (sql_d, sql_t, sql_w) - return 'SELECT count(*) FROM %s%s' % (sql_t, sql_w) + return 'SELECT count(DISTINCT %s) FROM %s%s;' % (sql_d, sql_t, sql_w) + return 'SELECT count(*) FROM %s%s;' % (sql_t, sql_w) def count(self,query,distinct=None): self.execute(self._count(query,distinct)) return self.cursor.fetchone()[0] @@ -1213,13 +1260,13 @@ def tables(self,query): tables = set() if isinstance(query, Field): tables.add(query.tablename) elif isinstance(query, (Expression, Query)): - if query.first!=None: + if not query.first is None: tables = tables.union(self.tables(query.first)) - if query.second!=None: + if not query.second is None: tables = tables.union(self.tables(query.second)) return list(tables) def commit(self): return self.connection.commit() @@ -1278,11 +1325,11 @@ if obj is None: return 'NULL' if obj == '' and not fieldtype[:2] in ['st', 'te', 'pa', 'up']: return 'NULL' r = self.represent_exceptions(obj,fieldtype) - if r != None: + if not r is None: return r if fieldtype == 'boolean': if obj and not str(obj)[:1].upper() in ['F', '0']: return "'T'" else: @@ -1324,11 +1371,11 @@ obj = str(obj) try: obj.decode(self.db_codec) except: obj = obj.decode('latin1').encode(self.db_codec) - return "'%s'" % obj.replace("'", "''") + return self.adapt(obj) def represent_exceptions(self, obj, fieldtype): return None def lastrowid(self,table): @@ -1448,20 +1495,34 @@ colset.update_record = lambda _ = (colset, table, id), **a: update_record(_, a) colset.delete_record = lambda t = table, i = id: t._db(t._id==i).delete() for (referee_table, referee_name) in \ table._referenced_by: s = db[referee_table][referee_name] - if not referee_table in colset: - # for backward compatibility - colset[referee_table] = Set(db, s == id) - ### add new feature? - ### colset[referee_table+'_by_'+refree_name] = Set(db, s == id) + referee_link = db._referee_name and \ + db._referee_name % dict(table=referee_table,field=referee_name) + if referee_link and not referee_link in colset: + colset[referee_link] = Set(db, s == id) colset['id'] = id new_rows.append(new_row) + rowsobj = Rows(db, new_rows, colnames, rawrows=rows) + for tablename in virtualtables: - for item in db[tablename].virtualfields: + ### new style virtual fields + table = db[tablename] + fields_virtual = [(f,v) for (f,v) in table.items() if isinstance(v,FieldVirtual)] + fields_lazy = [(f,v) for (f,v) in table.items() if isinstance(v,FieldLazy)] + if fields_virtual or fields_lazy: + for row in rowsobj.records: + box = row[tablename] + for f,v in fields_virtual: + box[f] = v.f(row) + for f,v in fields_lazy: + box[f] = (v.handler or VirtualCommand)(v.f,row) + + ### old style virtual fields + for item in table.virtualfields: try: rowsobj = rowsobj.setvirtualfields(**{tablename:item}) except KeyError: # to avoid breaking virtualfields when partial select pass @@ -1471,12 +1532,16 @@ fieldname = self.db._request_tenant for tablename in tablenames: table = self.db[tablename] if fieldname in table: default = table[fieldname].default - if default!=None: - query = query&(table[fieldname]==default) + if not default is None: + newquery = table[fieldname]==default + if query is None: + query = newquery + else: + query = query&newquery return query ################################################################################### # List of all the available adapters, they all extend BaseAdapter ################################################################################### @@ -1504,11 +1569,11 @@ except: return None def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "sqlite" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -1521,14 +1586,15 @@ dbpath = uri.split('://')[1] if dbpath[0] != '/': dbpath = os.path.join(self.folder.decode(path_encoding).encode('utf8'),dbpath) if not 'check_same_thread' in driver_args: driver_args['check_same_thread'] = False + if not 'detect_types' in driver_args: + driver_args['detect_types'] = self.driver.PARSE_DECLTYPES def connect(dbpath=dbpath, driver_args=driver_args): return self.driver.Connection(dbpath, **driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() self.connection.create_function('web2py_extract', 2, SQLiteAdapter.web2py_extract) def _truncate(self,table,mode = ''): tablename = table._tablename return ['DELETE FROM %s;' % tablename, @@ -1542,11 +1608,11 @@ driver = globals().get('zxJDBC',None) def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "sqlite" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -1559,17 +1625,15 @@ dbpath = uri.split('://')[1] if dbpath[0] != '/': dbpath = os.path.join(self.folder.decode(path_encoding).encode('utf8'),dbpath) def connect(dbpath=dbpath,driver_args=driver_args): return self.driver.connect(java.sql.DriverManager.getConnection('jdbc:sqlite:'+dbpath),**driver_args) - self.pool_connection(connect) - self.cursor = self.connection.cursor() # FIXME http://www.zentus.com/sqlitejdbc/custom_functions.html for UDFs # self.connection.create_function('web2py_extract', 2, SQLiteAdapter.web2py_extract) - def execute(self,a): - return self.log_execute(a[:-1]) + def execute(self,a): + return self.log_execute(a) class MySQLAdapter(BaseAdapter): driver = globals().get('pymysql',None) @@ -1622,11 +1686,11 @@ def concat_add(self,table): return '; ALTER TABLE %s ADD ' % table def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "mysql" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -1658,18 +1722,16 @@ port=port, charset=charset)) def connect(driver_args=driver_args): return self.driver.connect(**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() self.execute('SET FOREIGN_KEY_CHECKS=1;') self.execute("SET sql_mode='NO_BACKSLASH_ESCAPES';") def lastrowid(self,table): self.execute('select last_insert_id();') return int(self.cursor.fetchone()[0]) - class PostgreSQLAdapter(BaseAdapter): driver = globals().get('psycopg2',None) @@ -1692,10 +1754,13 @@ 'list:integer': 'TEXT', 'list:string': 'TEXT', 'list:reference': 'TEXT', } + def adapt(self,obj): + return psycopg2_adapt(obj).getquoted() + def sequence_name(self,table): return '%s_id_Seq' % table def RANDOM(self): return 'RANDOM()' @@ -1719,11 +1784,11 @@ # % (table._tablename, table._fieldname, table._sequence_name)) self.execute(query) def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "postgres" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -1757,13 +1822,10 @@ % (db, user, host, port, password) def connect(msg=msg,driver_args=driver_args): return self.driver.connect(msg,**driver_args) self.pool_connection(connect) self.connection.set_client_encoding('UTF8') - self.cursor = self.connection.cursor() - self.execute('BEGIN;') - self.execute("SET CLIENT_ENCODING TO 'UNICODE';") self.execute("SET standard_conforming_strings=on;") def lastrowid(self,table): self.execute("select currval('%s')" % table._sequence_name) return int(self.cursor.fetchone()[0]) @@ -1786,11 +1848,11 @@ class JDBCPostgreSQLAdapter(PostgreSQLAdapter): def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "postgres" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -1816,11 +1878,10 @@ msg = ('jdbc:postgresql://%s:%s/%s' % (host, port, db), user, password) def connect(msg=msg,driver_args=driver_args): return self.driver.connect(*msg,**driver_args) self.pool_connection(connect) self.connection.set_client_encoding('UTF8') - self.cursor = self.connection.cursor() self.execute('BEGIN;') self.execute("SET CLIENT_ENCODING TO 'UNICODE';") class OracleAdapter(BaseAdapter): @@ -1903,11 +1964,11 @@ return "to_date('%s','yyyy-mm-dd hh24:mi:ss')" % obj return None def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "oracle" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -1917,11 +1978,10 @@ if not 'threaded' in driver_args: driver_args['threaded']=True def connect(uri=uri,driver_args=driver_args): return self.driver.connect(uri,**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() self.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS';") self.execute("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS';") oracle_fix = re.compile("[^']*('[^']*'[^']*)*\:(?PCLOB\('([^']+|'')*'\))") def execute(self, command): @@ -1932,11 +1992,13 @@ if not m: break command = command[:m.start('clob')] + str(i) + command[m.end('clob'):] args.append(m.group('clob')[6:-2].replace("''", "'")) i += 1 - return self.log_execute(command[:-1], args) + if command[-1:]==';': + command = command[:-1] + return self.log_execute(command, args) def create_sequence_and_triggers(self, query, table, **args): tablename = table._tablename sequence_name = table._sequence_name trigger_name = table._trigger_name @@ -2064,11 +2126,10 @@ % (host, port, db, user, password, urlargs) def connect(cnxn=cnxn,driver_args=driver_args): return self.driver.connect(cnxn,**driver_args) if not fake_connect: self.pool_connection(connect) - self.cursor = self.connection.cursor() def lastrowid(self,table): #self.execute('SELECT @@IDENTITY;') self.execute('SELECT SCOPE_IDENTITY();') return int(self.cursor.fetchone()[0]) @@ -2170,11 +2231,11 @@ return ['DELETE FROM %s;' % table._tablename, 'SET GENERATOR %s TO 0;' % table._sequence_name] def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "firebird" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -2206,15 +2267,14 @@ if adapter_args['driver_name'] == 'kinterbasdb': self.driver = kinterbasdb elif adapter_args['driver_name'] == 'firebirdsql': self.driver = firebirdsql else: - self.driver = kinterbasdb + self.driver = kinterbasdb def connect(driver_args=driver_args): return self.driver.connect(**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() def create_sequence_and_triggers(self, query, table, **args): tablename = table._tablename sequence_name = table._sequence_name trigger_name = table._trigger_name @@ -2231,11 +2291,11 @@ class FireBirdEmbeddedAdapter(FireBirdAdapter): def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "firebird" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -2270,15 +2330,14 @@ if adapter_args['driver_name'] == 'kinterbasdb': self.driver = kinterbasdb elif adapter_args['driver_name'] == 'firebirdsql': self.driver = firebirdsql else: - self.driver = kinterbasdb + self.driver = kinterbasdb def connect(driver_args=driver_args): return self.driver.connect(**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() class InformixAdapter(BaseAdapter): driver = globals().get('informixdb',None) @@ -2341,11 +2400,11 @@ return "to_date('%s','yyyy-mm-dd hh24:mi:ss')" % obj return None def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "informix" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -2373,11 +2432,10 @@ dsn = '%s@%s' % (db,host) driver_args.update(dict(user=user,password=password,autocommit=True)) def connect(dsn=dsn,driver_args=driver_args): return self.driver.connect(dsn,**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() def execute(self,command): if command[-1:]==';': command = command[:-1] return self.log_execute(command) @@ -2439,11 +2497,11 @@ return "'%s'" % obj return None def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "db2" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -2451,11 +2509,10 @@ self.find_or_make_work_folder() cnxn = uri.split('://', 1)[1] def connect(cnxn=cnxn,driver_args=driver_args): return self.driver.connect(cnxn,**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() def execute(self,command): if command[-1:]==';': command = command[:-1] return self.log_execute(command) @@ -2497,11 +2554,11 @@ } def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "teradata" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -2509,11 +2566,10 @@ self.find_or_make_work_folder() cnxn = uri.split('://', 1)[1] def connect(cnxn=cnxn,driver_args=driver_args): return self.driver.connect(cnxn,**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() INGRES_SEQNAME='ii***lineitemsequence' # NOTE invalid database object name # (ANSI-SQL wants this form of name # to be a delimited identifier) @@ -2561,11 +2617,11 @@ sql_o += ' OFFSET %d' % (lmin, ) return 'SELECT %s %s FROM %s%s%s;' % (sql_s, sql_f, sql_t, sql_w, sql_o) def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "ingres" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -2585,11 +2641,10 @@ servertype=servertype, trace=trace)) def connect(driver_args=driver_args): return self.driver.connect(**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() def create_sequence_and_triggers(self, query, table, **args): # post create table auto inc code (if needed) # modify table to btree for performance.... # Older Ingres releases could use rule/trigger like Oracle above. @@ -2682,11 +2737,11 @@ % (table._tablename, table._id.name, table._sequence_name)) self.execute(query) def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "sapdb" self.uri = uri self.pool_size = pool_size self.folder = folder @@ -2711,12 +2766,10 @@ def connect(user=user,password=password,database=db, host=host,driver_args=driver_args): return self.driver.Connection(user,password,database, host,**driver_args) self.pool_connection(connect) - # self.connection.set_client_encoding('UTF8') - self.cursor = self.connection.cursor() def lastrowid(self,table): self.execute("select %s.NEXTVAL from dual" % table._sequence_name) return int(self.cursor.fetchone()[0]) @@ -2756,11 +2809,10 @@ user=credential_decoder(user), passwd=credential_decoder(password), def connect(host,port,db,user,passwd,driver_args=driver_args): return self.driver.connect(host,port,db,user,passwd,**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() self.execute('SET FOREIGN_KEY_CHECKS=1;') self.execute("SET sql_mode='NO_BACKSLASH_ESCAPES';") ######## GAE MySQL ########## @@ -2767,10 +2819,13 @@ class DatabaseStoredFile: web2py_filesystem = False + def escape(self,obj): + return self.db._adapter.esacpe(obj) + def __init__(self,db,filename,mode): if db._adapter.dbengine != 'mysql': raise RuntimeError, "only MySQL can store metadata .table files in database for now" self.db = db self.filename = filename @@ -2779,11 +2834,12 @@ self.db.executesql("CREATE TABLE IF NOT EXISTS web2py_filesystem (path VARCHAR(512), content LONGTEXT, PRIMARY KEY(path) ) ENGINE=InnoDB;") DatabaseStoredFile.web2py_filesystem = True self.p=0 self.data = '' if mode in ('r','rw','a'): - query = "SELECT content FROM web2py_filesystem WHERE path='%s'" % filename + query = "SELECT content FROM web2py_filesystem WHERE path='%s'" \ + % filename rows = self.db.executesql(query) if rows: self.data = rows[0][0] elif os.path.exists(filename): datafile = open(filename, 'r') @@ -2809,21 +2865,23 @@ def write(self,data): self.data += data def close(self): - self.db.executesql("DELETE FROM web2py_filesystem WHERE path='%s'" % self.filename) - query = "INSERT INTO web2py_filesystem(path,content) VALUES ('%s','%s')" % \ - (self.filename, self.data.replace("'","''")) + self.db.executesql("DELETE FROM web2py_filesystem WHERE path=%s" \ + % self.adapt(self.filename)) + query = "INSERT INTO web2py_filesystem(path,content) VALUES (%s,%s)"\ + % (self.adapt(self.filename), self.adapt(self.data)) self.db.executesql(query) self.db.commit() @staticmethod def exists(db,filename): if os.path.exists(filename): return True - query = "SELECT path FROM web2py_filesystem WHERE path='%s'" % filename + query = "SELECT path FROM web2py_filesystem WHERE path=%s" \ + % self.adapt(filename) if db.executesql(query): return True return False @@ -2843,15 +2901,14 @@ self.db.executesql(query) self.db.commit() class GoogleSQLAdapter(UseDatabaseStoredFile,MySQLAdapter): - def __init__(self, db, uri='google:sql://realm:domain/database', pool_size=0, - folder=None, db_codec='UTF-8', check_reserved=None, - migrate=True, fake_migrate=False, + def __init__(self, db, uri='google:sql://realm:domain/database', + pool_size=0, folder=None, db_codec='UTF-8', credential_decoder = lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "mysql" self.uri = uri self.pool_size = pool_size @@ -2863,17 +2920,17 @@ if not m: raise SyntaxError, "Invalid URI string in SQLDB: %s" % self._uri instance = credential_decoder(m.group('instance')) db = credential_decoder(m.group('db')) driver_args['instance'] = instance - if not migrate: + createdb = adapter_args.get('createdb',True) + if not createdb: driver_args['database'] = db def connect(driver_args=driver_args): return rdbms.connect(**driver_args) self.pool_connection(connect) - self.cursor = self.connection.cursor() - if migrate: + if createdb: # self.execute('DROP DATABASE %s' % db) self.execute('CREATE DATABASE IF NOT EXISTS %s' % db) self.execute('USE %s' % db) self.execute("SET FOREIGN_KEY_CHECKS=1;") self.execute("SET sql_mode='NO_BACKSLASH_ESCAPES';") @@ -2903,11 +2960,11 @@ obj = [] if not isinstance(obj, (list, tuple)): obj = [obj] if obj == '' and not fieldtype[:2] in ['st','te','pa','up']: return None - if obj != None: + if not obj is None: if isinstance(obj, list) and not fieldtype.startswith('list'): obj = [self.represent(o, fieldtype) for o in obj] elif fieldtype in ('integer','id'): obj = long(obj) elif fieldtype == 'double': @@ -3044,11 +3101,11 @@ def file_open(self, filename, mode='rb', lock=True): pass def file_close(self, fileobj, unlock=True): pass def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.types.update({ 'boolean': gae.BooleanProperty, 'string': (lambda: gae.StringProperty(multiline=True)), 'text': gae.TextProperty, 'password': gae.StringProperty, @@ -3226,11 +3283,13 @@ return self.expand(first) def truncate(self,table,mode): self.db(table._id > 0).delete() - def select_raw(self,query,fields=[],attributes={}): + def select_raw(self,query,fields=None,attributes=None): + fields = fields or [] + attributes = attributes or {} new_fields = [] for item in fields: if isinstance(item,SQLALL): new_fields += item.table else: @@ -3420,11 +3479,11 @@ return repr(not isinstance(value,unicode) and value or value.encode('utf8')) def __init__(self,db,uri='couchdb://127.0.0.1:5984', pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.uri = uri self.dbengine = 'couchdb' self.folder = folder db['_lastsql'] = '' @@ -3432,11 +3491,11 @@ self.pool_size = pool_size url='http://'+uri[10:] def connect(url=url,driver_args=driver_args): return couchdb.Server(url,**driver_args) - self.pool_connection(connect) + self.pool_connection(connect,cursor=False) def create_table(self, table, migrate=True, fake_migrate=False, polymodel=None): if migrate: try: self.connection.create(table._tablename) @@ -3580,22 +3639,22 @@ } def __init__(self,db,uri='mongodb://127.0.0.1:5984/db', pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=lambda x:x, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.uri = uri self.dbengine = 'mongodb' self.folder = folder db['_lastsql'] = '' self.db_codec = 'UTF-8' self.pool_size = pool_size - m = re.compile('^(?P[^\:/]+)(\:(?P[0-9]+))?/(?P.+)$').match(self._uri[10:]) + m = re.compile('^(?P[^\:/]+)(\:(?P[0-9]+))?/(?P.+)$').match(self.uri[10:]) if not m: - raise SyntaxError, "Invalid URI string in DAL: %s" % self._uri + raise SyntaxError, "Invalid URI string in DAL: %s" % self.uri host = m.group('host') if not host: raise SyntaxError, 'mongodb: host name required' dbname = m.group('db') if not dbname: @@ -3602,11 +3661,11 @@ raise SyntaxError, 'mongodb: db name required' port = m.group('port') or 27017 driver_args.update(dict(host=host,port=port)) def connect(dbname=dbname,driver_args=driver_args): return pymongo.Connection(**driver_args)[dbname] - self.pool_connection(connect) + self.pool_connection(connect,cursor=False) def insert(self,table,fields): ctable = self.connection[table._tablename] values = dict((k,self.represent(v,table[k].type)) for k,v in fields) ctable.insert(values) @@ -3706,11 +3765,11 @@ requires.append(validators.IS_DATETIME()) elif field.db and field_type.startswith('reference') and \ field_type.find('.') < 0 and \ field_type[10:] in field.db.tables: referenced = field.db[field_type[10:]] - def repr_ref(id, r=referenced, f=ff): return f(r, id) + def repr_ref(id, row=None, r=referenced, f=ff): return f(r, id) field.represent = field.represent or repr_ref if hasattr(referenced, '_format') and referenced._format: requires = validators.IS_IN_DB(field.db,referenced._id, referenced._format) if field.unique: @@ -3720,11 +3779,11 @@ return requires elif field.db and field_type.startswith('list:reference') and \ field_type.find('.') < 0 and \ field_type[15:] in field.db.tables: referenced = field.db[field_type[15:]] - def list_ref_repr(ids, r=referenced, f=ff): + def list_ref_repr(ids, row=None, r=referenced, f=ff): if not ids: return None refs = r._db(r._id.belongs(ids)).select(r._id) return (refs and ', '.join(str(f(r,ref.id)) for ref in refs) or '') field.represent = field.represent or list_ref_repr @@ -3736,11 +3795,11 @@ multiple=True) if field.unique: requires._and = validators.IS_NOT_IN_DB(field.db,field) return requires elif field_type.startswith('list:'): - def repr_list(values): return', '.join(str(v) for v in (values or [])) + def repr_list(values,row=None): return', '.join(str(v) for v in (values or [])) field.represent = field.represent or repr_list if field.unique: requires.insert(0,validators.IS_NOT_IN_DB(field.db,field)) sff = ['in', 'do', 'da', 'ti', 'de', 'bo'] if field.notnull and not field_type[:2] in sff: @@ -3770,12 +3829,18 @@ this is only used to store a Row """ def __getitem__(self, key): key=str(key) + m = table_field.match(key) if key in self.get('_extra',{}): return self._extra[key] + elif m: + try: + return dict.__getitem__(self, m.group(1))[m.group(2)] + except (KeyError,TypeError): + key = m.group(2) return dict.__getitem__(self, key) def __call__(self,key): return self.__getitem__(key) @@ -3843,10 +3908,115 @@ class SQLCallableList(list): def __call__(self): return copy.copy(self) +def smart_query(fields,text): + if not isinstance(fields,(list,tuple)): + fields = [fields] + new_fields = [] + for field in fields: + if isinstance(field,Field): + new_fields.append(field) + elif isinstance(field,Table): + for ofield in field: + new_fields.append(ofield) + else: + raise RuntimeError, "fields must be a list of fields" + field_map = {} + for field in fields: + n = field.name.lower() + if not n in field_map: + field_map[n] = field + n = str(field).lower() + if not n in field_map: + field_map[n] = field + re_constants = re.compile('(\"[^\"]*?\")|(\'[^\']*?\')') + constants = {} + i = 0 + while True: + m = re_constants.search(text) + if not m: break + text = text[:m.start()]+('#%i' % i)+text[m.end():] + constants[str(i)] = m.group()[1:-1] + i+=1 + text = re.sub('\s+',' ',text).lower() + for a,b in [('&','and'), + ('|','or'), + ('~','not'), + ('==','=='), + ('<','<'), + ('>','>'), + ('<=','<='), + ('>=','>='), + ('<>','!='), + ('=<','<='), + ('=>','>='), + ('=','=='), + (' less or equal than ','<='), + (' greater or equal than ','>='), + (' equal or less than ','<='), + (' equal or greater than ','>='), + (' less or equal ','<='), + (' greater or equal ','>='), + (' equal or less ','<='), + (' equal or greater ','>='), + (' not equal to ','!='), + (' not equal ','!='), + (' equal to ','=='), + (' equal ','=='), + (' equals ','!='), + (' less than ','<'), + (' greater than ','>'), + (' starts with ','startswith'), + (' ends with ','endswith'), + (' is ','==')]: + if a[0]==' ': + text = text.replace(' is'+a,' %s ' % b) + text = text.replace(a,' %s ' % b) + text = re.sub('\s+',' ',text).lower() + query = field = neg = op = logic = None + for item in text.split(): + if field is None: + if item == 'not': + neg = True + elif not neg and not logic and item in ('and','or'): + logic = item + elif item in field_map: + field = field_map[item] + else: + raise RuntimeError, "Invalid syntax" + elif not field is None and op is None: + op = item + elif not op is None: + if item.startswith('#'): + if not item[1:] in constants: + raise RuntimeError, "Invalid syntax" + value = constants[item[1:]] + else: + value = item + if op == '==': op = 'like' + if op == '==': new_query = field==value + elif op == '<': new_query = field': new_query = field>value + elif op == '<=': new_query = field<=value + elif op == '>=': new_query = field>=value + elif op == 'contains': new_query = field.contains(value) + elif op == 'like': new_query = field.like(value) + elif op == 'startswith': new_query = field.startswith(value) + elif op == 'endswith': new_query = field.endswith(value) + else: raise RuntimeError, "Invalid operation" + if neg: new_query = ~new_query + if query is None: + query = new_query + elif logic == 'and': + query &= new_query + elif logic == 'or': + query |= new_query + field = op = neg = logic = None + return query + class DAL(dict): """ an instance of this class represents a database connection @@ -3902,16 +4072,17 @@ for (i, db) in instances: db._adapter.commit_prepared(keys[i]) return - def __init__(self, uri='sqlite://dummy.db', pool_size=0, folder=None, + def __init__(self, uri='sqlite://dummy.db', + pool_size=0, folder=None, db_codec='UTF-8', check_reserved=None, migrate=True, fake_migrate=False, migrate_enabled=True, fake_migrate_all=False, decode_credentials=False, driver_args=None, - adapter_args={}, attempts=5, auto_import=False): + adapter_args=None, attempts=5, auto_import=False): """ Creates a new Database Abstraction Layer instance. Keyword arguments: @@ -3948,10 +4119,11 @@ self._lastsql = '' self._timings = [] self._pending_references = {} self._request_tenant = 'request_tenant' self._common_fields = [] + self._referee_name = '%(table)s' if not str(attempts).isdigit() or attempts < 0: attempts = 5 if uri: uris = isinstance(uri,(list,tuple)) and uri or [uri] error = '' @@ -3962,12 +4134,15 @@ if is_jdbc and not uri.startswith('jdbc:'): uri = 'jdbc:'+uri self._dbname = regex_dbname.match(uri).group() if not self._dbname in ADAPTERS: raise SyntaxError, "Error in URI '%s' or database not supported" % self._dbname - # notice that driver args or {} else driver_args defaults to {} global, not correct - args = (self,uri,pool_size,folder,db_codec,credential_decoder,driver_args or {}, adapter_args) + # notice that driver args or {} else driver_args + # defaults to {} global, not correct + args = (self,uri,pool_size,folder, + db_codec, credential_decoder, + driver_args or {}, adapter_args or {}) self._adapter = ADAPTERS[self._dbname](*args) connected = True break except SyntaxError: raise @@ -4266,14 +4441,19 @@ 'primarykey', 'fake_migrate', 'format', 'trigger_name', 'sequence_name', - 'polymodel']: - raise SyntaxError, 'invalid table "%s" attribute: %s' % (tablename, key) - migrate = self._migrate_enabled and args.get('migrate',self._migrate) - fake_migrate = self._fake_migrate_all or args.get('fake_migrate',self._fake_migrate) + 'polymodel', + 'table_class']: + raise SyntaxError, 'invalid table "%s" attribute: %s' \ + % (tablename, key) + migrate = self._migrate_enabled and args.get('migrate', + self._migrate) + fake_migrate = self._fake_migrate_all or args.get('fake_migrate', + self._fake_migrate) + table_class = args.get('table_class',Table) format = args.get('format',None) trigger_name = args.get('trigger_name', None) sequence_name = args.get('sequence_name', None) primarykey=args.get('primarykey',None) polymodel=args.get('polymodel',None) @@ -4291,14 +4471,14 @@ self.check_reserved_keyword(tablename) if self._common_fields: fields = [f for f in fields] + [f for f in self._common_fields] - t = self[tablename] = Table(self, tablename, *fields, - **dict(primarykey=primarykey, - trigger_name=trigger_name, - sequence_name=sequence_name)) + t = self[tablename] = table_class(self, tablename, *fields, + **dict(primarykey=primarykey, + trigger_name=trigger_name, + sequence_name=sequence_name)) # db magic if self._uri in (None,'None'): return t t._create_references() @@ -4337,10 +4517,13 @@ self[key] = value def __repr__(self): return '' + def smart_query(self,fields,text): + return Set(self, smart_query(fields,text)) + def __call__(self, query=None): if isinstance(query,Table): query = query._id>0 elif isinstance(query,Field): query = query!=None @@ -4407,12 +4590,13 @@ ofile.write('TABLE %s\r\n' % table) self(self[table]._id > 0).select().export_to_csv_file(ofile, *args, **kwargs) ofile.write('\r\n\r\n') ofile.write('END') - def import_from_csv_file(self, ifile, id_map={}, null='', + def import_from_csv_file(self, ifile, id_map=None, null='', unique='uuid', *args, **kwargs): + if id_map is None: id_map={} for line in ifile: line = line.strip() if not line: continue elif line == 'END': @@ -4452,10 +4636,13 @@ if key == 'id': return int(self) self.__allocate() return self._record.get(key, None) + def get(self, key): + return self.__getattr__(key) + def __setattr__(self, key, value): if key.startswith('_'): int.__setattr__(self, key, value) return self.__allocate() @@ -4526,11 +4713,11 @@ if primarykey: if not isinstance(primarykey,list): raise SyntaxError, \ "primarykey must be a list of fields from table '%s'" \ % tablename - self._primarykey = primarykey + self._primarykey = primarykey elif not [f for f in fields if isinstance(f,Field) and f.type=='id']: field = Field('id', 'id') newfields.append(field) fieldnames.add('id') self._id = field @@ -4567,11 +4754,11 @@ and field.uploadfield is True: tmp = field.uploadfield = '%s_blob' % field.name fields.append(self._db.Field(tmp, 'blob', default='')) lower_fieldnames = set() - reserved = dir(Table) + ['fields'] + reserved = dir(Table) + ['fields'] for field in fields: if db and db.check_reserved: db.check_reserved_keyword(field.name) elif field.name in reserved: raise SyntaxError, "field name %s not allowed" % field.name @@ -4587,11 +4774,11 @@ if field.type == 'id': self['id'] = field field.tablename = field._tablename = tablename field.table = field._table = self field.db = field._db = self._db - if self._db and field.type!='text' and \ + if self._db and not field.type in ('text','blob') and \ self._db._adapter.maxcharlength < field.length: field.length = self._db._adapter.maxcharlength if field.requires == DEFAULT: field.requires = sqlhtml_validators(field) self.ALL = SQLALL(self) @@ -4602,10 +4789,13 @@ raise SyntaxError, \ "primarykey must be a list of fields from table '%s " % tablename else: self[k].notnull = True + def update(self,*args,**kwargs): + raise RuntimeError, "Syntax Not Supported" + def _validate(self,**vars): errors = Row() for key,value in vars.items(): value,error = self[key].validate(value) if error: @@ -4763,13 +4953,13 @@ else: new_fields.append((self[name],fields[name])) new_fields_names.append(name) for ofield in self: if not ofield.name in new_fields_names: - if not update and ofield.default!=None: + if not update and not ofield.default is None: new_fields.append((ofield,ofield.default)) - elif update and ofield.update!=None: + elif update and not ofield.update is None: new_fields.append((ofield,ofield.update)) for ofield in self: if not ofield.name in new_fields_names and ofield.compute: try: new_fields.append((ofield,ofield.compute(Row(fields)))) @@ -4845,10 +5035,11 @@ if not self._tablename in id_map: id_map[self._tablename] = {} id_map_self = id_map[self._tablename] def fix(field, value, id_map): + list_reference_s='list:reference' if value == null: value = None elif field.type=='blob': value = base64.b64decode(value) elif field.type=='double': @@ -4861,12 +5052,12 @@ value = None else: value = int(value) elif field.type.startswith('list:string'): value = bar_decode_string(value) - elif field.type.startswith('list:reference'): - ref_table = field.type[10:].strip() + elif field.type.startswith(list_reference_s): + ref_table = field.type[len(list_reference_s):].strip() value = [id_map[ref_table][int(v)] \ for v in bar_decode_string(value)] elif field.type.startswith('list:'): value = bar_decode_integer(value) elif id_map and field.type.startswith('reference'): @@ -4910,11 +5101,11 @@ record.update_record(**dict(items)) new_id = record[self._id.name] else: new_id = self.insert(**dict(items)) if id_map and cid != []: - id_map_self[line[cid]] = new_id + id_map_self[int(line[cid])] = new_id def with_alias(self, alias): return self._db._adapter.alias(self,alias) def on(self, query): @@ -4974,12 +5165,15 @@ return Expression(self.db, self.db._adapter.EXTRACT, self, 'hour', 'integer') def minutes(self): return Expression(self.db, self.db._adapter.EXTRACT, self, 'minute', 'integer') + def coalesce(self,*others): + return Expression(self.db, self.db._adapter.COALESCE, self, others, self.type) + def coalesce_zero(self): - return Expression(self.db, self.db._adapter.COALESCE_ZERO, self, None, self.type) + return Expression(self.db, self.db._adapter.COALESCE_ZERO, self, None, self.type) def seconds(self): return Expression(self.db, self.db._adapter.EXTRACT, self, 'second', 'integer') def __getslice__(self, start, stop): @@ -5064,11 +5258,14 @@ def endswith(self, value): if not self.type in ('string', 'text'): raise SyntaxError, "endswith used with incompatible field type" return Query(self.db, self.db._adapter.ENDSWITH, self, value) - def contains(self, value): + def contains(self, value, all=False): + if isinstance(value,(list,tuple)): + subqueries = [self.contains(str(v).strip()) for v in value if str(v).strip()] + return reduce(all and AND or OR, subqueries) if not self.type in ('string', 'text') and not self.type.startswith('list:'): raise SyntaxError, "contains used with incompatible field type" return Query(self.db, self.db._adapter.CONTAINS, self, value) def with_alias(self,alias): @@ -5130,12 +5327,24 @@ return None def __str__(self): return self._class +class FieldVirtual(object): + def __init__(self,f): + self.f = f + +class FieldLazy(object): + def __init__(self,f,handler=None): + self.f = f + self.handler = handler + class Field(Expression): + + Virtual = FieldVirtual + Lazy = FieldLazy """ an instance of this class represents a database field example:: @@ -5179,11 +5388,11 @@ ondelete='CASCADE', notnull=False, unique=False, uploadfield=True, widget=None, - label=None, + label=DEFAULT, comment=None, writable=True, readable=True, update=None, authorize=None, @@ -5213,11 +5422,11 @@ regex_python_keywords.match(fieldname): raise SyntaxError, 'Field: invalid field name: %s' % fieldname if isinstance(type, Table): type = 'reference ' + type._tablename self.type = type # 'string', 'integer' - self.length = (length is None) and MAXCHARLENGTH or length + self.length = (length is None) and DEFAULTLENGTH.get(type,512) or length if default==DEFAULT: self.default = update or None else: self.default = default self.required = required # is this field required @@ -5226,19 +5435,22 @@ self.unique = unique self.uploadfield = uploadfield self.uploadfolder = uploadfolder self.uploadseparate = uploadseparate self.widget = widget - self.label = label or ' '.join(item.capitalize() for item in fieldname.split('_')) + if label == DEFAULT: + self.label = ' '.join(i.capitalize() for i in fieldname.split('_')) + else: + self.label = label or '' self.comment = comment self.writable = writable self.readable = readable self.update = update self.authorize = authorize self.autodelete = autodelete if not represent and type in ('list:integer','list:string'): - represent=lambda x: ', '.join(str(y) for y in x or []) + represent=lambda x,r=None: ', '.join(str(y) for y in x or []) self.represent = represent self.compute = compute self.isattachment = True self.custom_store = custom_store self.custom_retrieve = custom_retrieve @@ -5369,10 +5581,12 @@ return '%s.%s' % (self.tablename, self.name) except: return '.%s' % self.name +def raw(s): return Expression(None,s) + class Query(object): """ a query object necessary to define a set. it can be stored or can be passed to DAL.__call__() to obtain a Set @@ -5390,11 +5604,11 @@ db, op, first=None, second=None, ): - self.db = db + self.db = self._db = db self.op = op self.first = first self.second = second def __str__(self): @@ -5447,10 +5661,12 @@ self.query = query def __call__(self, query): if isinstance(query,Table): query = query._id>0 + elif isinstance(query,str): + query = raw(query) elif isinstance(query,Field): query = query!=None if self.query: return Set(self.db, self.query & query) else: @@ -5490,15 +5706,15 @@ fields = self.db[tablename]._listify(update_fields,update=True) if not fields: raise SyntaxError, "No fields to update" self.delete_uploaded_files(update_fields) return self.db._adapter.update(tablename,self.query,fields) - + def validate_and_update(self, **update_fields): tablename = self.db._adapter.get_table(self.query) response = Row() - response.errors = self.db[tablename]._validate(**update_fields) + response.errors = self.db[tablename]._validate(**update_fields) fields = self.db[tablename]._listify(update_fields,update=True) if not fields: raise SyntaxError, "No fields to update" self.delete_uploaded_files(update_fields) if not response.errors: @@ -5535,23 +5751,34 @@ uploadfolder = os.path.join(self.db._adapter.folder, '..', 'uploads') if field.uploadseparate: items = oldname.split('.') uploadfolder = os.path.join(uploadfolder, "%s.%s" % (items[0], items[1]), - items[2][:2]) + items[2][:2]) oldpath = os.path.join(uploadfolder, oldname) if os.path.exists(oldpath): os.unlink(oldpath) -def update_record(pack, a={}): +def update_record(pack, a=None): (colset, table, id) = pack b = a or dict(colset) c = dict([(k,v) for (k,v) in b.items() if k in table.fields and table[k].type!='id']) table._db(table._id==id).update(**c) for (k, v) in c.items(): colset[k] = v +class VirtualCommand(object): + def __init__(self,method,row): + self.method=method + #self.instance=instance + self.row=row + def __call__(self,*args,**kwargs): + return self.method(self.row,*args,**kwargs) + +def lazy_virtualfield(f): + f.__lazy__ = True + return f class Rows(object): """ A wrapper for the return value of a select. It basically represents a table. @@ -5573,24 +5800,46 @@ self.colnames = colnames self.compact = compact self.response = rawrows def setvirtualfields(self,**keyed_virtualfields): + """ + db.define_table('x',Field('number','integer')) + if db(db.x).isempty(): [db.x.insert(number=i) for i in range(10)] + + from gluon.dal import lazy_virtualfield + + class MyVirtualFields(object): + # normal virtual field (backward compatible, discouraged) + def normal_shift(self): return self.x.number+1 + # lazy virtual field (because of @staticmethod) + @lazy_virtualfield + def lazy_shift(instance,row,delta=4): return row.x.number+delta + db.x.virtualfields.append(MyVirtualFields()) + + for row in db(db.x).select(): + print row.number, row.normal_shift, row.lazy_shift(delta=7) + """ if not keyed_virtualfields: return self for row in self.records: for (tablename,virtualfields) in keyed_virtualfields.items(): attributes = dir(virtualfields) - virtualfields.__dict__.update(row) if not tablename in row: box = row[tablename] = Row() else: box = row[tablename] + updated = False for attribute in attributes: if attribute[0] != '_': method = getattr(virtualfields,attribute) - if hasattr(method,'im_func') and method.im_func.func_code.co_argcount: + if hasattr(method,'__lazy__'): + box[attribute]=VirtualCommand(method,row) + elif type(method)==types.MethodType: + if not updated: + virtualfields.__dict__.update(row) + updated = True box[attribute]=method() return self def __and__(self,other): if self.colnames!=other.colnames: raise Exception, 'Cannot & incompatible Rows objects' @@ -5779,11 +6028,11 @@ field = self.db[t][f] if isinstance(record.get(t, None), (Row,dict)): value = record[t][f] else: value = record[f] - if field.type=='blob' and value!=None: + if field.type=='blob' and not value is None: value = base64.b64encode(value) elif represent and field.represent: value = field.represent(value) row.append(none_exception(value)) writer.writerow(row) @@ -5806,18 +6055,20 @@ def inner_loop(record, col): (t, f) = col.split('.') res = None if not table_field.match(col): + key = col res = record._extra[col] else: + key = f if isinstance(record.get(t, None), Row): res = record[t][f] else: res = record[f] if mode == 'object': - return (f, res) + return (key, res) else: return res if mode == 'object': items = [dict([inner_loop(record, col) for col in @@ -6056,8 +6307,10 @@ ################################################################################ if __name__ == '__main__': import doctest doctest.testmod() + + ADDED gluon/dal.pyc Index: gluon/dal.pyc ================================================================== --- gluon/dal.pyc +++ gluon/dal.pyc cannot compute difference between binary files Index: gluon/debug.py ================================================================== --- gluon/debug.py +++ gluon/debug.py @@ -78,8 +78,10 @@ if data is None: break result.append(data) logger.info("DEBUG: result %s" % repr(result)) return ''.join(result) + + Index: gluon/decoder.py ================================================================== --- gluon/decoder.py +++ gluon/decoder.py @@ -70,6 +70,8 @@ return encoding def decoder(buffer): encoding = autoDetectXMLEncoding(buffer) return buffer.decode(encoding).encode('utf8') + + ADDED gluon/decoder.pyc Index: gluon/decoder.pyc ================================================================== --- gluon/decoder.pyc +++ gluon/decoder.pyc cannot compute difference between binary files Index: gluon/fileutils.py ================================================================== --- gluon/fileutils.py +++ gluon/fileutils.py @@ -11,16 +11,18 @@ import os import re import tarfile import glob import time +import datetime from http import HTTP from gzip import open as gzopen from settings import global_settings __all__ = [ + 'parse_version', 'read_file', 'write_file', 'readlines_file', 'up', 'abspath', @@ -38,10 +40,18 @@ 'w2p_pack_plugin', 'w2p_unpack_plugin', 'fix_newlines', 'make_fake_file_like_object', ] + +def parse_version(version = "Version 1.99.0 (2011-09-19 08:23:26)"): + re_version = re.compile('[^\d]+ (\d+)\.(\d+)\.(\d+)\s*\((?P.+?)\)\s*(?P[a-z]+)?') + m = re_version.match(version) + a,b,c = int(m.group(1)),int(m.group(2)),int(m.group(3)), + s = m.group('type') or 'dev' + d = datetime.datetime.strptime(m.group('datetime'),'%Y-%m-%d %H:%M:%S') + return (a,b,c,d,s) def read_file(filename, mode='r'): "returns content from filename, making sure to close the file explicitly on exit." f = open(filename, mode) try: @@ -382,6 +392,8 @@ def write(self, value): pass def close(self): pass return LogFile() + + ADDED gluon/fileutils.pyc Index: gluon/fileutils.pyc ================================================================== --- gluon/fileutils.pyc +++ gluon/fileutils.pyc cannot compute difference between binary files Index: gluon/globals.py ================================================================== --- gluon/globals.py +++ gluon/globals.py @@ -72,15 +72,17 @@ self.vars = Storage() self.folder = None self.application = None self.function = None self.args = List() - self.extension = None + self.extension = 'html' self.now = datetime.datetime.now() + self.utcnow = datetime.datetime.utcnow() self.is_restful = False self.is_https = False self.is_local = False + self.global_settings = settings.global_settings def compute_uuid(self): self.uuid = '%s/%s.%s.%s' % ( self.application, self.client.replace(':', '_'), @@ -100,10 +102,12 @@ def f(_action=action,_self=self,*a,**b): self.is_restful = True method = _self.env.request_method if len(_self.args) and '.' in _self.args[-1]: _self.args[-1],_self.extension = _self.args[-1].rsplit('.',1) + current.response.headers['Content-Type'] = \ + contenttype(_self.extension.lower()) if not method in ['GET','POST','DELETE','PUT']: raise HTTP(400,"invalid method") rest_action = _action().get(method,None) if not rest_action: raise HTTP(400,"method not supported") @@ -180,10 +184,27 @@ else: run_view_in(self._view_environment) page = self.body.getvalue() return page + def include_meta(self): + s = '' + for key,value in (self.meta or {}).items(): + s += '' % (key,xmlescape(value)) + self.write(s,escape=False) + + def include_files(self): + s = '' + for k,f in enumerate(self.files or []): + if not f in self.files[:k]: + filename = f.lower().split('?')[0] + if filename.endswith('.css'): + s += '' % f + elif filename.endswith('.js'): + s += '' % f + self.write(s,escape=False) + def stream( self, stream, chunk_size = DEFAULT_CHUNK_SIZE, request=None, ADDED gluon/globals.pyc Index: gluon/globals.pyc ================================================================== --- gluon/globals.pyc +++ gluon/globals.pyc cannot compute difference between binary files Index: gluon/highlight.py ================================================================== --- gluon/highlight.py +++ gluon/highlight.py @@ -21,17 +21,17 @@ def __init__( self, mode, link=None, - styles={}, + styles=None, ): """ Initialise highlighter: mode = language (PYTHON, WEB2PY,C, CPP, HTML, HTML_PLAIN) """ - + styles = styles or {} mode = mode.upper() if link and link[-1] != '/': link = link + '/' self.link = link self.styles = styles @@ -164,11 +164,11 @@ + r'print|raise|return|try|except|global|assert|lambda|' + r'yield|for|while|if|elif|else|and|in|is|not|or|import|' + r'from|True|False)(?![a-zA-Z0-9_])'), 'color:#185369; font-weight: bold'), ('WEB2PY', - re.compile(r'(request|response|session|cache|redirect|local_import|HTTP|TR|XML|URL|BEAUTIFY|A|BODY|BR|B|CAT|CENTER|CODE|DIV|EM|EMBED|FIELDSET|LEGEND|FORM|H1|H2|H3|H4|H5|H6|IFRAME|HEAD|HR|HTML|I|IMG|INPUT|LABEL|LI|LINK|MARKMIN|MENU|META|OBJECT|OL|ON|OPTION|P|PRE|SCRIPT|SELECT|SPAN|STYLE|TABLE|THEAD|TBODY|TFOOT|TAG|TD|TEXTAREA|TH|TITLE|TT|T|UL|XHTML|IS_SLUG|IS_STRONG|IS_LOWER|IS_UPPER|IS_ALPHANUMERIC|IS_DATETIME|IS_DATETIME_IN_RANGE|IS_DATE|IS_DATE_IN_RANGE|IS_DECIMAL_IN_RANGE|IS_EMAIL|IS_EXPR|IS_FLOAT_IN_RANGE|IS_IMAGE|IS_INT_IN_RANGE|IS_IN_SET|IS_IPV4|IS_LIST_OF|IS_LENGTH|IS_MATCH|IS_EQUAL_TO|IS_EMPTY_OR|IS_NULL_OR|IS_NOT_EMPTY|IS_TIME|IS_UPLOAD_FILENAME|IS_URL|CLEANUP|CRYPT|IS_IN_DB|IS_NOT_IN_DB|DAL|Field|SQLFORM|SQLTABLE|xmlescape|embed64)(?![a-zA-Z0-9_])' + re.compile(r'(request|response|session|cache|redirect|local_import|HTTP|TR|XML|URL|BEAUTIFY|A|BODY|BR|B|CAT|CENTER|CODE|COL|COLGROUP|DIV|EM|EMBED|FIELDSET|LEGEND|FORM|H1|H2|H3|H4|H5|H6|IFRAME|HEAD|HR|HTML|I|IMG|INPUT|LABEL|LI|LINK|MARKMIN|MENU|META|OBJECT|OL|ON|OPTION|P|PRE|SCRIPT|SELECT|SPAN|STYLE|TABLE|THEAD|TBODY|TFOOT|TAG|TD|TEXTAREA|TH|TITLE|TT|T|UL|XHTML|IS_SLUG|IS_STRONG|IS_LOWER|IS_UPPER|IS_ALPHANUMERIC|IS_DATETIME|IS_DATETIME_IN_RANGE|IS_DATE|IS_DATE_IN_RANGE|IS_DECIMAL_IN_RANGE|IS_EMAIL|IS_EXPR|IS_FLOAT_IN_RANGE|IS_IMAGE|IS_INT_IN_RANGE|IS_IN_SET|IS_IPV4|IS_LIST_OF|IS_LENGTH|IS_MATCH|IS_EQUAL_TO|IS_EMPTY_OR|IS_NULL_OR|IS_NOT_EMPTY|IS_TIME|IS_UPLOAD_FILENAME|IS_URL|CLEANUP|CRYPT|IS_IN_DB|IS_NOT_IN_DB|DAL|Field|SQLFORM|SQLTABLE|xmlescape|embed64)(?![a-zA-Z0-9_])' ), 'link:%(link)s;text-decoration:None;color:#FF5C1F;'), ('MAGIC', re.compile(r'self|None'), 'color:#185369; font-weight: bold'), ('MULTILINESTRING', re.compile(r'r?u?(\'\'\'|""")'), 'color: #FF9966'), @@ -219,11 +219,11 @@ % dict(link=self.link)) else: new_mode = \ Highlighter.all_styles[mode][0](self, token, match, style) - if new_mode != None: + if not new_mode is None: mode = new_mode i += max(1, len(match.group())) break else: self.change_style(None, None) @@ -239,26 +239,28 @@ if token in self.styles: style = self.styles[token] if self.span_style != style: if style != 'Keep': - if self.span_style != None: + if not self.span_style is None: self.output.append('') - if style != None: + if not style is None: self.output.append('' % style) self.span_style = style def highlight( code, language, link='/examples/globals/vars/', counter=1, - styles={}, + styles=None, highlight_line=None, - attributes={}, + attributes=None, ): + styles = styles or {} + attributes = attributes or {} if not 'CODE' in styles: code_style = """ font-size: 11px; font-family: Bitstream Vera Sans Mono,monospace; background-color: transparent; @@ -312,11 +314,11 @@ code = '
    '.join(lines) numbers = '
    '.join(linenumbers) items = attributes.items() fa = ' '.join([key[1:].lower() for (key, value) in items if key[:1] - == '_' and value == None] + ['%s="%s"' + == '_' and value is None] + ['%s="%s"' % (key[1:].lower(), str(value).replace('"', "'")) for (key, value) in attributes.items() if key[:1] == '_' and value]) if fa: fa = ' ' + fa @@ -329,6 +331,8 @@ argfp = open(sys.argv[1]) data = argfp.read() argfp.close() print '' + highlight(data, sys.argv[2])\ + '' + + ADDED gluon/highlight.pyc Index: gluon/highlight.pyc ================================================================== --- gluon/highlight.pyc +++ gluon/highlight.pyc cannot compute difference between binary files Index: gluon/html.py ================================================================== --- gluon/html.py +++ gluon/html.py @@ -44,10 +44,12 @@ 'BR', 'BUTTON', 'CENTER', 'CAT', 'CODE', + 'COL', + 'COLGROUP', 'DIV', 'EM', 'EMBED', 'FIELDSET', 'FORM', @@ -128,12 +130,12 @@ def URL( a=None, c=None, f=None, r=None, - args=[], - vars={}, + args=None, + vars=None, anchor='', extension=None, env=None, hmac_key=None, hash_vars=True, @@ -140,10 +142,11 @@ salt=None, user_signature=None, scheme=None, host=None, port=None, + encode_embedded_slash=False, ): """ generate a URL example:: @@ -163,11 +166,17 @@ >>> str(URL(a='a', c='c', f='f', anchor='1+2')) '/a/c/f#1%2B2' >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], ... vars={'p':(1,3), 'q':2}, anchor='1', hmac_key='key')) - '/a/c/f/x/y/z?p=1&p=3&q=2&_signature=5d06bb8a4a6093dd325da2ee591c35c61afbd3c6#1' + '/a/c/f/x/y/z?p=1&p=3&q=2&_signature=a32530f0d0caa80964bb92aad2bedf8a4486a31f#1' + + >>> str(URL(a='a', c='c', f='f', args=['w/x', 'y/z'])) + '/a/c/f/w/x/y/z' + + >>> str(URL(a='a', c='c', f='f', args=['w/x', 'y/z'], encode_embedded_slash=True)) + '/a/c/f/w%2Fx/y%2Fz' generates a url '/a/c/f' corresponding to application a, controller c and function f. If r=request is passed, a, c, f are set, respectively, to r.application, r.controller, r.function. @@ -232,11 +241,19 @@ if not (application and controller and function): raise SyntaxError, 'not enough information to build the url' if not isinstance(args, (list, tuple)): args = [args] - other = args and urllib.quote('/' + '/'.join([str(x) for x in args])) or '' + + if args: + if encode_embedded_slash: + other = '/' + '/'.join([urllib.quote(str(x), '') for x in args]) + else: + other = args and urllib.quote('/' + '/'.join([str(x) for x in args])) + else: + other = '' + if other.endswith('/'): other += '/' # add trailing slash to make last trailing empty arg explicit if vars.has_key('_signature'): vars.pop('_signature') list_vars = [] @@ -268,11 +285,11 @@ h_vars = [(k, v) for (k, v) in list_vars if k in hash_vars] # re-assembling the same way during hash authentication message = h_args + '?' + urllib.urlencode(sorted(h_vars)) - sig = hmac_hash(message,hmac_key,salt=salt) + sig = hmac_hash(message, hmac_key, digest_alg='sha1', salt=salt) # add the signature into vars list_vars.append(('_signature', sig)) if list_vars: other += '?%s' % urllib.urlencode(list_vars) @@ -305,12 +322,12 @@ URL.verify(hmac_key='...') the key has to match the one used to generate the URL. >>> r = Storage() - >>> gv = Storage(p=(1,3),q=2,_signature='5d06bb8a4a6093dd325da2ee591c35c61afbd3c6') - >>> r.update(dict(application='a', controller='c', function='f')) + >>> gv = Storage(p=(1,3),q=2,_signature='a32530f0d0caa80964bb92aad2bedf8a4486a31f') + >>> r.update(dict(application='a', controller='c', function='f', extension='html')) >>> r['args'] = ['x', 'y', 'z'] >>> r['get_vars'] = gv >>> verifyURL(r, 'key') True >>> verifyURL(r, 'kay') @@ -381,11 +398,11 @@ return False # build the full message string with both args & vars message = h_args + '?' + urllib.urlencode(sorted(h_vars)) # hash with the hmac_key provided - sig = hmac_hash(message,str(hmac_key),salt=salt) + sig = hmac_hash(message, str(hmac_key), digest_alg='sha1', salt=salt) # put _signature back in get_vars just in case a second call to URL.verify is performed # (otherwise it'll immediately return false) request.get_vars['_signature'] = original_sig @@ -996,11 +1013,11 @@ if not sibs: return None return sibs[0] class CAT(DIV): - + tag = '' def TAG_unpickler(data): return cPickle.loads(data) @@ -1275,22 +1292,33 @@ class A(DIV): tag = 'a' def xml(self): - if self['callback']: - self['_onclick']="ajax('%s',[],'%s');return false;" % \ - (self['callback'],self['target'] or '') + if self['delete']: + d = "jQuery(this).closest('%s').remove();" % self['delete'] + else: + d = '' + if self['component']: + self['_onclick']="web2py_component('%s','%s');%sreturn false;" % \ + (self['component'],self['target'] or '',d) + self['_href'] = self['_href'] or '#null' + elif self['callback']: + if d: + self['_onclick']="if(confirm(w2p_ajax_confirm_message||'Are you sure you want o delete this object?')){ajax('%s',[],'%s');%s};return false;" % (self['callback'],self['target'] or '',d) + else: + self['_onclick']="ajax('%s',[],'%s');%sreturn false;" % \ + (self['callback'],self['target'] or '',d) self['_href'] = self['_href'] or '#null' elif self['cid']: self['_onclick']='web2py_component("%s","%s");return false;' % \ (self['_href'],self['cid']) return DIV.xml(self) class BUTTON(DIV): - + tag = 'button' class EM(DIV): @@ -1534,11 +1562,11 @@ if requires: if not isinstance(requires, (list, tuple)): requires = [requires] for validator in requires: (value, errors) = validator(value) - if errors != None: + if not errors is None: self.vars[name] = value self.errors[name] = errors break if not name in self.errors: self.vars[name] = value @@ -1549,34 +1577,34 @@ t = self['_type'] if not t: t = self['_type'] = 'text' t = t.lower() value = self['value'] - if self['_value'] == None: + if self['_value'] is None: _value = None else: _value = str(self['_value']) - if t == 'checkbox': + if t == 'checkbox' and not '_checked' in self.attributes: if not _value: _value = self['_value'] = 'on' if not value: value = [] elif value is True: value = [_value] elif not isinstance(value,(list,tuple)): value = str(value).split('|') self['_checked'] = _value in value and 'checked' or None - elif t == 'radio': + elif t == 'radio' and not '_checked' in self.attributes: if str(value) == str(_value): self['_checked'] = 'checked' else: self['_checked'] = None elif t == 'text' or t == 'hidden': - if value != None: - self['_value'] = value - else: + if value is None: self['value'] = _value + else: + self['_value'] = value def xml(self): name = self.attributes.get('_name', None) if name and hasattr(self, 'errors') \ and self.errors.get(name, None) \ @@ -1602,11 +1630,11 @@ def _postprocessing(self): if not '_rows' in self.attributes: self['_rows'] = 10 if not '_cols' in self.attributes: self['_cols'] = 40 - if self['value'] != None: + if not self['value'] is None: self.components = [self['value']] elif self.components: self['value'] = self.components[0] @@ -1668,11 +1696,11 @@ else: component_list.append([c]) options = itertools.chain(*component_list) value = self['value'] - if value != None: + if not value is None: if not self['_multiple']: for c in options: # my patch if value and str(c['_value'])==str(value): c['_selected'] = 'selected' else: @@ -1725,25 +1753,30 @@ def __init__(self, *components, **attributes): DIV.__init__(self, *components, **attributes) self.vars = Storage() self.errors = Storage() self.latest = Storage() + self.accepted = None # none for not submitted def accepts( self, - vars, + request_vars, session=None, formname='default', keepvalues=False, onvalidation=None, hideerror=False, + **kwargs ): - if vars.__class__.__name__ == 'Request': - vars=vars.post_vars + """ + kwargs is not used but allows to specify the same interface for FROM and SQLFORM + """ + if request_vars.__class__.__name__ == 'Request': + request_vars=request_vars.post_vars self.errors.clear() self.request_vars = Storage() - self.request_vars.update(vars) + self.request_vars.update(request_vars) self.session = session self.formname = formname self.keepvalues = keepvalues # if this tag is a form and we are in accepting mode (status=True) @@ -1767,28 +1800,29 @@ if isinstance(onvalidation, dict): onsuccess = onvalidation.get('onsuccess', None) onfailure = onvalidation.get('onfailure', None) if onsuccess and status: onsuccess(self) - if onfailure and vars and not status: + if onfailure and request_vars and not status: onfailure(self) status = len(self.errors) == 0 elif status: if isinstance(onvalidation, (list, tuple)): [f(self) for f in onvalidation] else: onvalidation(self) if self.errors: status = False - if session != None: + if not session is None: if hasattr(self,'record_hash'): formkey = self.record_hash else: formkey = web2py_uuid() self.formkey = session['_formkey[%s]' % formname] = formkey if status and not keepvalues: self._traverse(False,hideerror) + self.accepted = status return status def _postprocessing(self): if not '_action' in self.attributes: self['_action'] = '' @@ -1816,24 +1850,13 @@ hidden_fields = self.hidden_fields() if hidden_fields.components: newform.append(hidden_fields) return DIV.xml(newform) - def validate(self, - values=None, - session=None, - formname='default', - keepvalues=False, - onvalidation=None, - hideerror=False, - onsuccess='flash', - onfailure='flash', - message_onsuccess=None, - message_onfailure=None, - ): + def validate(self,**kwargs): """ - This function validates the form, + This function validates the form, you can use it instead of directly form.accepts. Usage: In controller @@ -1840,45 +1863,64 @@ def action(): form=FORM(INPUT(_name=\"test\", requires=IS_NOT_EMPTY())) form.validate() #you can pass some args here - see below return dict(form=form) - This can receive a bunch of arguments + This can receive a bunch of arguments onsuccess = 'flash' - will show message_onsuccess in response.flash None - will do nothing can be a function (lambda form: pass) onfailure = 'flash' - will show message_onfailure in response.flash None - will do nothing can be a function (lambda form: pass) - - values = values to test the validation - dictionary, response.vars, session or other - Default to (request.vars, session) message_onsuccess message_onfailure + next = where to redirect in case of success + any other kwargs will be passed for form.accepts(...) """ - from gluon import current - if not session: session = current.session - if not values: values = current.request.post_vars - - message_onsuccess = message_onsuccess or current.T("Success!") - message_onfailure = message_onfailure or \ - current.T("Errors in form, please check it out.") - - if self.accepts(values, session): + from gluon import current, redirect + kwargs['request_vars'] = kwargs.get('request_vars',current.request.post_vars) + kwargs['session'] = kwargs.get('session',current.session) + kwargs['dbio'] = kwargs.get('dbio',False) # necessary for SQLHTML forms + + onsuccess = kwargs.get('onsuccess','flash') + onfailure = kwargs.get('onfailure','flash') + message_onsuccess = kwargs.get('message_onsuccess', + current.T("Success!")) + message_onfailure = kwargs.get('message_onfailure', + current.T("Errors in form, please check it out.")) + next = kwargs.get('next',None) + for key in ('message_onsuccess','message_onfailure','onsuccess', + 'onfailure','next'): + if key in kwargs: + del kwargs[key] + + if self.accepts(**kwargs): if onsuccess == 'flash': - current.response.flash = message_onsuccess + if next: + current.session.flash = message_onsuccess + else: + current.response.flash = message_onsuccess elif callable(onsuccess): onsuccess(self) + if next: + if self.vars.id: + next = next.replace('[id]',str(self.vars.id)) + next = next % self.vars + if not next.startswith('/'): + next = URL(next) + redirect(next) return True elif self.errors: if onfailure == 'flash': current.response.flash = message_onfailure elif callable(onfailure): onfailure(self) return False - def process(self, values=None, session=None, **args): + def process(self, **kwargs): """ Perform the .validate() method but returns the form Usage in controllers: # directly on return @@ -1901,12 +1943,13 @@ # after argument can be 'flash' to response.flash messages # or a function name to use as callback or None to do nothing. def action(): return dict(form=SQLFORM(db.table).process(onsuccess=my_callback) - """ - self.validate(values=values, session=session, **args) + """ + kwargs['dbio'] = kwargs.get('dbio',True) # necessary for SQLHTML forms + self.validate(**kwargs) return self class BEAUTIFY(DIV): @@ -2162,11 +2205,12 @@ self.parent = self.parent.parent except: raise RuntimeError, "unable to balance tag %s" % tagname if parent_tagname[:len(tagname)]==tagname: break -def markdown_serializer(text,tag=None,attr={}): +def markdown_serializer(text,tag=None,attr=None): + attr = attr or {} if tag is None: return re.sub('\s+',' ',text) if tag=='br': return '\n\n' if tag=='h1': return '#'+text+'\n\n' if tag=='h2': return '#'*2+text+'\n\n' if tag=='h3': return '#'*3+text+'\n\n' @@ -2177,11 +2221,12 @@ if tag=='tt' or tag=='code': return '`%s`' % text if tag=='a': return '[%s](%s)' % (text,attr.get('_href','')) if tag=='img': return '![%s](%s)' % (attr.get('_alt',''),attr.get('_src','')) return text -def markmin_serializer(text,tag=None,attr={}): +def markmin_serializer(text,tag=None,attr=None): + attr = attr or {} # if tag is None: return re.sub('\s+',' ',text) if tag=='br': return '\n\n' if tag=='h1': return '# '+text+'\n\n' if tag=='h2': return '#'*2+' '+text+'\n\n' if tag=='h3': return '#'*3+' '+text+'\n\n' @@ -2202,14 +2247,14 @@ class MARKMIN(XmlComponent): """ For documentation: http://web2py.com/examples/static/markmin.html """ - def __init__(self, text, extra={}, allowed={}, sep='p'): + def __init__(self, text, extra=None, allowed=None, sep='p'): self.text = text - self.extra = extra - self.allowed = allowed + self.extra = extra or {} + self.allowed = allowed or {} self.sep = sep def xml(self): """ calls the gluon.contrib.markmin render function to convert the wiki syntax @@ -2234,6 +2279,8 @@ if __name__ == '__main__': import doctest doctest.testmod() + + ADDED gluon/html.pyc Index: gluon/html.pyc ================================================================== --- gluon/html.pyc +++ gluon/html.pyc cannot compute difference between binary files Index: gluon/http.py ================================================================== --- gluon/http.py +++ gluon/http.py @@ -118,10 +118,14 @@ "stringify me" return self.message def redirect(location, how=303): + if not location: + return location = location.replace('\r', '%0D').replace('\n', '%0A') raise HTTP(how, 'You are being redirected here' % location, Location=location) + + ADDED gluon/http.pyc Index: gluon/http.pyc ================================================================== --- gluon/http.pyc +++ gluon/http.pyc cannot compute difference between binary files Index: gluon/import_all.py ================================================================== --- gluon/import_all.py +++ gluon/import_all.py @@ -105,6 +105,8 @@ if module in alert_dependency: msg = "Missing dependency: %(module)s\n" % locals() msg += "Try the following command: " msg += "easy_install-%(python_version)s -U %(module)s" % locals() raise ImportError, msg + + Index: gluon/languages.py ================================================================== --- gluon/languages.py +++ gluon/languages.py @@ -208,11 +208,11 @@ self.requested_languages = self.force(self.http_accept_language) self.lazy = True self.otherTs = {} def get_possible_languages(self): - possible_languages = self.current_languages + possible_languages = [lang for lang in self.current_languages] file_ending = re.compile("\.py$") for langfile in os.listdir(os.path.join(self.folder,'languages')): if file_ending.search(langfile): possible_languages.append(file_ending.sub('',langfile)) return possible_languages @@ -222,11 +222,11 @@ languages = languages[0] self.current_languages = languages self.force(self.http_accept_language) def force(self, *languages): - if not languages or languages[0] == None: + if not languages or languages[0] is None: languages = [] if len(languages) == 1 and isinstance(languages[0], (str, unicode)): languages = languages[0] if languages: if isinstance(languages, (str, unicode)): @@ -248,11 +248,11 @@ return languages self.language_file = None self.t = {} # ## no language by default return languages - def __call__(self, message, symbols={},language=None): + def __call__(self, message, symbols={}, language=None): if not language: if self.lazy: return lazyT(message, symbols, self) else: return self.translate(message, symbols) @@ -275,20 +275,23 @@ T('hello ## world ## token') -> 'hello ## world' the ## notation is ignored in multiline strings and strings that start with ##. this is to allow markmin syntax to be translated """ + #for some reason languages.py gets executed before gaehandler.py + # is able to set web2py_runtime_gae, so re-check here + is_gae = settings.global_settings.web2py_runtime_gae 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: tokens[0] = tokens[0].strip() message = tokens[0] + '##' + tokens[1].strip() mt = self.t.get(message, None) - if mt == None: + if mt is None: self.t[message] = mt = tokens[0] if self.language_file and not is_gae: write_dict(self.language_file, self.t) if symbols or symbols == 0: return mt % symbols @@ -342,7 +345,9 @@ if __name__ == '__main__': import doctest doctest.testmod() + + ADDED gluon/languages.pyc Index: gluon/languages.pyc ================================================================== --- gluon/languages.pyc +++ gluon/languages.pyc cannot compute difference between binary files Index: gluon/main.py ================================================================== --- gluon/main.py +++ gluon/main.py @@ -27,11 +27,11 @@ import socket import tempfile import random import string import platform -from fileutils import abspath, write_file +from fileutils import abspath, write_file, parse_version from settings import global_settings from admin import add_path_first, create_missing_folders, create_missing_app_folders from globals import current from custom_import import custom_import_install @@ -101,12 +101,13 @@ # pattern used to validate client address regex_client = re.compile('[\w\-:]+(\.[\w\-]+)*\.?') # ## to account for IPV6 version_info = open(abspath('VERSION', gluon=True), 'r') -web2py_version = version_info.read() +web2py_version = parse_version(version_info.read().strip()) version_info.close() +global_settings.web2py_version = web2py_version try: import rocket except: if not global_settings.web2py_runtime_gae: @@ -416,17 +417,18 @@ if not os.path.exists(request.folder): if request.application == rewrite.thread.routes.default_application and request.application != 'welcome': request.application = 'welcome' redirect(Url(r=request)) elif rewrite.thread.routes.error_handler: - redirect(Url(rewrite.thread.routes.error_handler['application'], - rewrite.thread.routes.error_handler['controller'], - rewrite.thread.routes.error_handler['function'], + _handler = rewrite.thread.routes.error_handler + redirect(Url(_handler['application'], + _handler['controller'], + _handler['function'], args=request.application)) else: - raise HTTP(404, - rewrite.thread.routes.error_message % 'invalid request', + raise HTTP(404, rewrite.thread.routes.error_message \ + % 'invalid request', web2py_error='invalid application') request.url = Url(r=request, args=request.args, extension=request.raw_extension) # ################################################## @@ -444,14 +446,16 @@ # ################################################## # expose wsgi hooks for convenience # ################################################## request.wsgi.environ = environ_aux(environ,request) - request.wsgi.start_response = lambda status='200', headers=[], \ + request.wsgi.start_response = \ + lambda status='200', headers=[], \ exec_info=None, response=response: \ start_response_aux(status, headers, exec_info, response) - request.wsgi.middleware = lambda *a: middleware_aux(request,response,*a) + request.wsgi.middleware = \ + lambda *a: middleware_aux(request,response,*a) # ################################################## # load cookies # ################################################## @@ -469,11 +473,12 @@ # ################################################## # set no-cache headers # ################################################## - response.headers['Content-Type'] = contenttype('.'+request.extension) + response.headers['Content-Type'] = \ + contenttype('.'+request.extension) response.headers['Cache-Control'] = \ 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' response.headers['Expires'] = \ time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime()) response.headers['Pragma'] = 'no-cache' @@ -548,12 +553,12 @@ response._custom_rollback() else: BaseAdapter.close_all_instances('rollback') http_response = \ - HTTP(500, - rewrite.thread.routes.error_message_ticket % dict(ticket=ticket), + HTTP(500, rewrite.thread.routes.error_message_ticket % \ + dict(ticket=ticket), web2py_error='ticket %s' % ticket) except: if request.body: @@ -571,16 +576,17 @@ except: pass e = RestrictedError('Framework', '', '', locals()) ticket = e.log(request) or 'unrecoverable' http_response = \ - HTTP(500, - rewrite.thread.routes.error_message_ticket % dict(ticket=ticket), + HTTP(500, rewrite.thread.routes.error_message_ticket \ + % dict(ticket=ticket), web2py_error='ticket %s' % ticket) finally: - if response and hasattr(response, 'session_file') and response.session_file: + if response and hasattr(response, 'session_file') \ + and response.session_file: response.session_file.close() # if global_settings.debugging: # import gluon.debug # gluon.debug.stop_trace() @@ -590,11 +596,11 @@ if not http_response: return wsgibase(new_environ,responder) if global_settings.web2py_crontype == 'soft': newcron.softcron(global_settings.applications_parent).start() return http_response.to(responder) - + def save_password(password, port): """ used by main() to save the password in the parameters_port.py file. """ @@ -716,10 +722,11 @@ pid_filename='httpserver.pid', log_filename='httpserver.log', profiler_filename=None, ssl_certificate=None, ssl_private_key=None, + ssl_ca_certificate=None, min_threads=None, max_threads=None, server_name=None, request_queue_size=5, timeout=10, @@ -767,10 +774,13 @@ logger.warning('unable to open SSL certificate. SSL is OFF') elif not os.path.exists(ssl_private_key): logger.warning('unable to open SSL private key. SSL is OFF') else: sock_list.extend([ssl_private_key, ssl_certificate]) + if ssl_ca_certificate: + sock_list.append(ssl_ca_certificate) + logger.info('SSL is ON') app_info = {'wsgi_app': appfactory(wsgibase, log_filename, profiler_filename) } @@ -805,6 +815,8 @@ self.server.stop(stoplogging) try: os.unlink(self.pid_filename) except: pass + + ADDED gluon/main.pyc Index: gluon/main.pyc ================================================================== --- gluon/main.pyc +++ gluon/main.pyc cannot compute difference between binary files Index: gluon/myregex.py ================================================================== --- gluon/myregex.py +++ gluon/myregex.py @@ -24,6 +24,8 @@ regex_include = re.compile(\ '(?P\{\{\s*include\s+[\'"](?P[^\'"]*)[\'"]\s*\}\})') regex_extend = re.compile(\ '^\s*(?P\{\{\s*extend\s+[\'"](?P[^\'"]+)[\'"]\s*\}\})',re.MULTILINE) + + ADDED gluon/myregex.pyc Index: gluon/myregex.pyc ================================================================== --- gluon/myregex.pyc +++ gluon/myregex.pyc cannot compute difference between binary files Index: gluon/newcron.py ================================================================== --- gluon/newcron.py +++ gluon/newcron.py @@ -93,11 +93,11 @@ stop == 0 if job started but did not yet complete if a cron job started within less than 60 seconds, acquire returns None if a cron job started before 60 seconds and did not stop, a warning is issue "Stale cron.master detected" """ - if portalocker.LOCK_EX == None: + if portalocker.LOCK_EX is None: logger.warning('WEB2PY CRON: Disabled because no file locking') return None self.master = open(self.path,'rb+') try: ret = None @@ -227,11 +227,11 @@ logger.debug('WEB2PY CRON Call returned success:\n%s' \ % stdoutdata) def crondance(applications_parent, ctype='soft', startup=False): apppath = os.path.join(applications_parent,'applications') - cron_path = os.path.join(apppath,'admin','cron') + cron_path = os.path.join(applications_parent) token = Token(cron_path) cronmaster = token.acquire(startup=startup) if not cronmaster: return now_s = time.localtime() @@ -308,6 +308,8 @@ except Exception, e: logger.warning( 'WEB2PY CRON: Execution error for %s: %s' \ % (task.get('cmd'), e)) token.release() + + ADDED gluon/newcron.pyc Index: gluon/newcron.pyc ================================================================== --- gluon/newcron.pyc +++ gluon/newcron.pyc cannot compute difference between binary files Index: gluon/portalocker.py ================================================================== --- gluon/portalocker.py +++ gluon/portalocker.py @@ -118,6 +118,8 @@ print 'Wrote lines. Hit enter to release lock.' dummy = sys.stdin.readline() log.close() + + ADDED gluon/portalocker.pyc Index: gluon/portalocker.pyc ================================================================== --- gluon/portalocker.pyc +++ gluon/portalocker.pyc cannot compute difference between binary files Index: gluon/reserved_sql_keywords.py ================================================================== --- gluon/reserved_sql_keywords.py +++ gluon/reserved_sql_keywords.py @@ -1709,6 +1709,8 @@ 'jdbc:postgres': JDBCPOSTGRESQL, 'common': COMMON, } ADAPTERS['all'] = reduce(lambda a,b:a.union(b),(x for x in ADAPTERS.values())) + + Index: gluon/restricted.py ================================================================== --- gluon/restricted.py +++ gluon/restricted.py @@ -110,33 +110,34 @@ def __init__( self, layer='', code='', output='', - environment={}, + environment=None, ): """ layer here is some description of where in the system the exception occurred. """ - + if environment is None: environment = {} self.layer = layer self.code = code self.output = output + self.environment = environment if layer: try: self.traceback = traceback.format_exc() except: self.traceback = 'no traceback because template parting error' try: - self.snapshot = snapshot(context=10,code=code,environment=environment) + self.snapshot = snapshot(context=10,code=code, + environment=self.environment) except: self.snapshot = {} else: self.traceback = '(no error)' self.snapshot = {} - self.environment = environment def log(self, request): """ logs the exception. """ @@ -175,16 +176,17 @@ """ The +'\n' is necessary else compile fails when code ends in a comment. """ return compile(code.rstrip().replace('\r\n','\n')+'\n', layer, 'exec') -def restricted(code, environment={}, layer='Unknown'): +def restricted(code, environment=None, layer='Unknown'): """ runs code in environment and returns the output. if an exception occurs in code it raises a RestrictedError containing the traceback. layer is passed to RestrictedError to identify where the error occurred. """ + if environment is None: environment = {} environment['__file__'] = layer try: if type(code) == types.CodeType: ccode = code else: @@ -281,6 +283,8 @@ for k,v in environment.items(): if k in ('request', 'response', 'session'): s[k] = BEAUTIFY(v) return s + + ADDED gluon/restricted.pyc Index: gluon/restricted.pyc ================================================================== --- gluon/restricted.pyc +++ gluon/restricted.pyc cannot compute difference between binary files Index: gluon/rewrite.py ================================================================== --- gluon/rewrite.py +++ gluon/rewrite.py @@ -37,11 +37,11 @@ default_application = 'init', applications = 'ALL', default_controller = 'default', controllers = 'DEFAULT', default_function = 'index', - functions = None, + functions = dict(), default_language = None, languages = None, root_static = ['favicon.ico', 'robots.txt'], domains = None, exclusive_domain = False, @@ -363,14 +363,10 @@ raise SyntaxError, "unknown key '%s' in router '%s'" % (key, app) if not router.controllers: router.controllers = set() elif not isinstance(router.controllers, str): router.controllers = set(router.controllers) - if router.functions: - router.functions = set(router.functions) - else: - router.functions = set() if router.languages: router.languages = set(router.languages) else: router.languages = set() if app != 'BASE': @@ -387,11 +383,19 @@ router.controllers.add(cname[:-3]) if router.controllers: router.controllers.add('static') router.controllers.add(router.default_controller) if router.functions: - router.functions.add(router.default_function) + if isinstance(router.functions, (set, tuple, list)): + functions = set(router.functions) + if isinstance(router.default_function, str): + functions.add(router.default_function) # legacy compatibility + router.functions = { router.default_controller: functions } + for controller in router.functions: + router.functions[controller] = set(router.functions[controller]) + else: + router.functions = dict() if isinstance(routers.BASE.applications, str) and routers.BASE.applications == 'ALL': routers.BASE.applications = list(all_apps) if routers.BASE.applications: routers.BASE.applications = set(routers.BASE.applications) @@ -423,15 +427,18 @@ for (domain, app) in [(d.strip(':'), a.strip('/')) for (d, a) in routers.BASE.domains.items()]: port = None if ':' in domain: (domain, port) = domain.split(':') ctlr = None + fcn = None if '/' in app: - (app, ctlr) = app.split('/') + (app, ctlr) = app.split('/', 1) + if ctlr and '/' in ctlr: + (ctlr, fcn) = ctlr.split('/') if app not in all_apps and app not in routers: raise SyntaxError, "unknown app '%s' in domains" % app - domains[(domain, port)] = (app, ctlr) + domains[(domain, port)] = (app, ctlr, fcn) routers.BASE.domains = domains def regex_uri(e, regexes, tag, default=None): "filter incoming URI against a list of regexes" path = e['PATH_INFO'] @@ -752,11 +759,11 @@ self.controller = None self.function = None self.extension = 'html' self.controllers = set() - self.functions = set() + self.functions = dict() self.languages = set() self.default_language = None self.map_hyphen = False self.exclusive_domain = False @@ -808,21 +815,24 @@ def map_app(self): "determine application name" base = routers.BASE # base router self.domain_application = None self.domain_controller = None + self.domain_function = None arg0 = self.harg0 - if base.applications and arg0 in base.applications: - self.application = arg0 - elif (self.host, self.port) in base.domains: - (self.application, self.domain_controller) = base.domains[(self.host, self.port)] + if (self.host, self.port) in base.domains: + (self.application, self.domain_controller, self.domain_function) = base.domains[(self.host, self.port)] + self.env['domain_application'] = self.application + self.env['domain_controller'] = self.domain_controller + self.env['domain_function'] = self.domain_function + elif (self.host, None) in base.domains: + (self.application, self.domain_controller, self.domain_function) = base.domains[(self.host, None)] self.env['domain_application'] = self.application self.env['domain_controller'] = self.domain_controller - elif (self.host, None) in base.domains: - (self.application, self.domain_controller) = base.domains[(self.host, None)] - self.env['domain_application'] = self.application - self.env['domain_controller'] = self.domain_controller + self.env['domain_function'] = self.domain_function + elif base.applications and arg0 in base.applications: + self.application = arg0 elif arg0 and not base.applications: self.application = arg0 else: self.application = base.default_application or '' self.pop_arg_if(self.application == arg0) @@ -926,12 +936,18 @@ return static_file def map_function(self): "handle function.extension" arg0 = self.harg0 # map hyphens - if not arg0 or self.functions and arg0 not in self.functions and self.controller == self.default_controller: - self.function = self.router.default_function or "" + functions = self.functions.get(self.controller, set()) + if isinstance(self.router.default_function, dict): + default_function = self.router.default_function.get(self.controller, None) + else: + default_function = self.router.default_function # str or None + default_function = self.domain_function or default_function + if not arg0 or functions and arg0 not in functions: + self.function = default_function or "" self.pop_arg_if(arg0 and self.function == arg0) else: func_ext = arg0.split('.') if len(func_ext) > 1: self.function = func_ext[0] @@ -1021,21 +1037,24 @@ self.host = host self.port = port self.applications = routers.BASE.applications self.controllers = self.router.controllers - self.functions = self.router.functions + self.functions = self.router.functions.get(self.controller, set()) self.languages = self.router.languages self.default_language = self.router.default_language self.exclusive_domain = self.router.exclusive_domain self.map_hyphen = self.router.map_hyphen self.map_static = self.router.map_static self.path_prefix = routers.BASE.path_prefix self.domain_application = request and self.request.env.domain_application self.domain_controller = request and self.request.env.domain_controller - self.default_function = self.router.default_function + if isinstance(self.router.default_function, dict): + self.default_function = self.router.default_function.get(self.controller, None) + else: + self.default_function = self.router.default_function if (self.router.exclusive_domain and self.domain_application and self.domain_application != self.application and not self.host): raise SyntaxError, 'cross-domain conflict: must specify host' lang = request and request.uri_language @@ -1060,11 +1079,11 @@ router = self.router # Handle the easy no-args case of tail-defaults: /a/c /a / # - if not self.args and self.function == router.default_function: + if not self.args and self.function == self.default_function: self.omit_function = True if self.controller == router.default_controller: self.omit_controller = True if self.application == self.default_application: self.omit_application = True @@ -1080,13 +1099,13 @@ # default_controller = ((self.application == self.domain_application) and self.domain_controller) or router.default_controller or '' if self.controller == default_controller: self.omit_controller = True - # omit function if default controller/function + # omit function if possible # - if self.functions and self.function == self.default_function and self.omit_controller: + if self.functions and self.function in self.functions and self.function == self.default_function: self.omit_function = True # prohibit ambiguous cases # # because we presume the lang string to be unambiguous, its presence protects application omission @@ -1203,11 +1222,11 @@ from URLs with function/args present, thus: /da/c/f/args => /c/f/args /da/dc/f/args => /f/args - We use [applications] and [controllers] and [functions] to suppress ambiguous omissions. + We use [applications] and [controllers] and {functions} to suppress ambiguous omissions. We assume that language names do not collide with a/c/f names. ''' map = MapUrlOut(request, env, application, controller, function, args, other, scheme, host, port) return map.acf() @@ -1215,6 +1234,8 @@ def get_effective_router(appname): "return a private copy of the effective router for the specified application" if not routers or appname not in routers: return None return Storage(routers[appname]) # return a copy + + ADDED gluon/rewrite.pyc Index: gluon/rewrite.pyc ================================================================== --- gluon/rewrite.pyc +++ gluon/rewrite.pyc cannot compute difference between binary files Index: gluon/rocket.py ================================================================== --- gluon/rocket.py +++ gluon/rocket.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- # This file is part of the Rocket Web Server -# Copyright (c) 2010 Timothy Farrell +# Copyright (c) 2011 Timothy Farrell # Import System Modules import sys import errno import socket import logging import platform +import traceback # Define Constants -VERSION = '1.2.2' +VERSION = '1.2.4' SERVER_NAME = socket.gethostname() SERVER_SOFTWARE = 'Rocket %s' % VERSION HTTP_SERVER_SOFTWARE = '%s Python/%s' % (SERVER_SOFTWARE, sys.version.split(' ')[0]) BUF_SIZE = 16384 SOCKET_TIMEOUT = 1 # in secs @@ -85,11 +86,14 @@ try: import ssl has_ssl = True except ImportError: has_ssl = False +# Import Package Modules # package imports removed in monolithic build +# TODO - This part is still very experimental. +#from .filelike import FileLikeSocket class Connection(object): __slots__ = [ 'setblocking', 'sendall', @@ -100,11 +104,15 @@ 'client_port', 'server_port', 'socket', 'start_time', 'ssl', - 'secure' + 'secure', + 'recv', + 'send', + 'read', + 'write' ] def __init__(self, sock_tuple, port, secure=False): self.client_addr, self.client_port = sock_tuple[1] self.server_port = port @@ -122,26 +130,267 @@ self.socket.settimeout(SOCKET_TIMEOUT) self.sendall = self.socket.sendall self.shutdown = self.socket.shutdown self.fileno = self.socket.fileno - self.makefile = self.socket.makefile self.setblocking = self.socket.setblocking + self.recv = self.socket.recv + self.send = self.socket.send + self.makefile = self.socket.makefile + +# FIXME - this is not ready for prime-time yet. +# def makefile(self, buf_size=BUF_SIZE): +# return FileLikeSocket(self, buf_size) def close(self): if hasattr(self.socket, '_sock'): try: self.socket._sock.close() except socket.error: info = sys.exc_info() - if info[1].errno != socket.EBADF: + if info[1].args[0] != socket.EBADF: raise info[1] else: pass self.socket.close() + # Monolithic build...end of module: rocket\connection.py +# Monolithic build...start of module: rocket\filelike.py + +# Import System Modules +import socket +try: + from io import StringIO +except ImportError: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO +# Import Package Modules +# package imports removed in monolithic build + +class FileLikeSocket(object): + def __init__(self, conn, buf_size=BUF_SIZE): + self.conn = conn + self.buf_size = buf_size + self.buffer = StringIO() + self.content_length = None + + if self.conn.socket.gettimeout() == 0.0: + self.read = self.non_blocking_read + else: + self.read = self.blocking_read + + def __iter__(self): + return self + + def recv(self, size): + while True: + try: + return self.conn.recv(size) + except socket.error: + exc = sys.exc_info() + e = exc[1] + # FIXME - Don't raise socket_errors_nonblocking or socket_error_eintr + if (e.args[0] not in set()): + raise + + def next(self): + data = self.readline() + if data == '': + raise StopIteration + return data + + def non_blocking_read(self, size=None): + # Shamelessly adapted from Cherrypy! + bufr = self.buffer + bufr.seek(0, 2) + if size is None: + while True: + data = self.recv(self.buf_size) + if not data: + break + bufr.write(data) + + self.buffer = StringIO() + + return bufr.getvalue() + else: + buf_len = self.buffer.tell() + if buf_len >= size: + bufr.seek(0) + data = bufr.read(size) + self.buffer = StringIO(bufr.read()) + return data + + self.buffer = StringIO() + while True: + remaining = size - buf_len + data = self.recv(remaining) + + if not data: + break + + n = len(data) + if n == size and not buf_len: + return data + + if n == remaining: + bufr.write(data) + del data + break + + bufr.write(data) + buf_len += n + del data + + return bufr.getvalue() + + def blocking_read(self, length=None): + if length is None: + if self.content_length is not None: + length = self.content_length + else: + length = 1 + + try: + data = self.conn.recv(length) + except: + data = b('') + + return data + + def readline(self): + data = b("") + char = self.read(1) + while char != b('\n') and char is not b(''): + line = repr(char) + data += char + char = self.read(1) + data += char + return data + + def readlines(self, hint="ignored"): + return list(self) + + def close(self): + self.conn = None + self.content_length = None + +# Monolithic build...end of module: rocket\filelike.py +# Monolithic build...start of module: rocket\futures.py + +# Import System Modules +import time +try: + from concurrent.futures import Future, ThreadPoolExecutor + from concurrent.futures.thread import _WorkItem + has_futures = True +except ImportError: + has_futures = False + + class Future: + pass + + class ThreadPoolExecutor: + pass + + class _WorkItem: + pass + + +class WSGIFuture(Future): + def __init__(self, f_dict, *args, **kwargs): + Future.__init__(self, *args, **kwargs) + + self.timeout = None + + self._mem_dict = f_dict + self._lifespan = 30 + self._name = None + self._start_time = time.time() + + def set_running_or_notify_cancel(self): + if time.time() - self._start_time >= self._lifespan: + self.cancel() + else: + return super(WSGIFuture, self).set_running_or_notify_cancel() + + + def remember(self, name, lifespan=None): + self._lifespan = lifespan or self._lifespan + + if name in self._mem_dict: + raise NameError('Cannot remember future by name "%s". ' % name + \ + 'A future already exists with that name.' ) + self._name = name + self._mem_dict[name] = self + + return self + + def forget(self): + if self._name in self._mem_dict and self._mem_dict[self._name] is self: + del self._mem_dict[self._name] + self._name = None + +class _WorkItem(object): + def __init__(self, future, fn, args, kwargs): + self.future = future + self.fn = fn + self.args = args + self.kwargs = kwargs + + def run(self): + if not self.future.set_running_or_notify_cancel(): + return + + try: + result = self.fn(*self.args, **self.kwargs) + except BaseException: + e = sys.exc_info()[1] + self.future.set_exception(e) + else: + self.future.set_result(result) + +class WSGIExecutor(ThreadPoolExecutor): + multithread = True + multiprocess = False + + def __init__(self, *args, **kwargs): + ThreadPoolExecutor.__init__(self, *args, **kwargs) + + self.futures = dict() + + def submit(self, fn, *args, **kwargs): + if self._shutdown_lock.acquire(): + if self._shutdown: + self._shutdown_lock.release() + raise RuntimeError('Cannot schedule new futures after shutdown') + + f = WSGIFuture(self.futures) + w = _WorkItem(f, fn, args, kwargs) + + self._work_queue.put(w) + self._adjust_thread_count() + self._shutdown_lock.release() + return f + else: + return False + +class FuturesMiddleware(object): + "Futures middleware that adds a Futures Executor to the environment" + def __init__(self, app, threads=5): + self.app = app + self.executor = WSGIExecutor(threads) + + def __call__(self, environ, start_response): + environ["wsgiorg.executor"] = self.executor + environ["wsgiorg.futures"] = self.executor.futures + return self.app(environ, start_response) + +# Monolithic build...end of module: rocket\futures.py # Monolithic build...start of module: rocket\listener.py # Import System Modules import os import socket @@ -170,13 +419,14 @@ # Instance variables self.active_queue = active_queue self.interface = interface self.addr = interface[0] self.port = interface[1] - self.secure = len(interface) == 4 and \ - os.path.exists(interface[2]) and \ - os.path.exists(interface[3]) + self.secure = len(interface) >= 4 + self.clientcert_req = (len(interface) == 5 and interface[4]) + + self.thread = None self.ready = False # Error Log self.err_log = logging.getLogger('Rocket.Errors.Port%i' % self.port) self.err_log.addHandler(NullHandler()) @@ -201,10 +451,16 @@ data = (interface[3], interface[0], interface[1]) self.err_log.error("Cannot find certificate file " "'%s'. Cannot bind to %s:%s" % data) return + if self.clientcert_req and not os.path.exists(interface[4]): + data = (interface[4], interface[0], interface[1]) + self.err_log.error("Cannot find root ca certificate file " + "'%s'. Cannot bind to %s:%s" % data) + return + # Set socket options try: listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except: msg = "Cannot share socket. Using %s:%i exclusively." @@ -236,29 +492,68 @@ self.ready = True def wrap_socket(self, sock): try: - sock = ssl.wrap_socket(sock, - keyfile = self.interface[2], - certfile = self.interface[3], - server_side = True, - ssl_version = ssl.PROTOCOL_SSLv23) + if self.clientcert_req: + ca_certs = self.interface[4] + cert_reqs = ssl.CERT_OPTIONAL + sock = ssl.wrap_socket(sock, + keyfile = self.interface[2], + certfile = self.interface[3], + server_side = True, + cert_reqs = cert_reqs, + ca_certs = ca_certs, + ssl_version = ssl.PROTOCOL_SSLv23) + else: + sock = ssl.wrap_socket(sock, + keyfile = self.interface[2], + certfile = self.interface[3], + server_side = True, + ssl_version = ssl.PROTOCOL_SSLv23) + except SSLError: # Generally this happens when an HTTP request is received on a # secure socket. We don't do anything because it will be detected # by Worker and dealt with appropriately. + self.err_log.error('SSL Error: %s' % traceback.format_exc()) pass return sock - - def run(self): + def start(self): if not self.ready: self.err_log.warning('Listener started when not ready.') return + if self.thread is not None and self.thread.isAlive(): + self.err_log.warning('Listener already running.') + return + + self.thread = Thread(target=self.listen, name="Port" + str(self.port)) + + self.thread.start() + + def isAlive(self): + if self.thread is None: + return False + + return self.thread.isAlive() + + def join(self): + if self.thread is None: + return + + self.ready = False + + self.thread.join() + + del self.thread + self.thread = None + self.ready = True + + def listen(self): if __debug__: self.err_log.debug('Entering main loop.') while True: try: sock, addr = self.listener.accept() @@ -279,11 +574,11 @@ self.err_log.debug('Listener exiting.') return else: continue except: - self.err_log.error(str(traceback.format_exc())) + self.err_log.error(traceback.format_exc()) # Monolithic build...end of module: rocket\listener.py # Monolithic build...start of module: rocket\main.py # Import System Modules @@ -290,11 +585,11 @@ import sys import time import socket import logging import traceback - +from threading import Lock try: from queue import Queue except ImportError: from Queue import Queue @@ -322,10 +617,12 @@ queue_size = None, timeout = 600, handle_signals = True): self.handle_signals = handle_signals + self.startstop_lock = Lock() + self.timeout = timeout if not isinstance(interfaces, list): self.interfaces = [interfaces] else: self.interfaces = interfaces @@ -346,24 +643,22 @@ queue_size = max_threads if isinstance(app_info, dict): app_info['server_software'] = SERVER_SOFTWARE - monitor_queue = Queue() - active_queue = Queue() - - self._monitor = Monitor(monitor_queue, active_queue, timeout) + self.monitor_queue = Queue() + self.active_queue = Queue() self._threadpool = ThreadPool(get_method(method), app_info = app_info, - active_queue=active_queue, - monitor_queue = monitor_queue, - min_threads=min_threads, - max_threads=max_threads) + active_queue = self.active_queue, + monitor_queue = self.monitor_queue, + min_threads = min_threads, + max_threads = max_threads) # Build our socket listeners - self.listeners = [Listener(i, queue_size, active_queue) for i in self.interfaces] + self.listeners = [Listener(i, queue_size, self.active_queue) for i in self.interfaces] for ndx in range(len(self.listeners)-1, 0, -1): if not self.listeners[ndx].ready: del self.listeners[ndx] if not self.listeners: @@ -376,79 +671,107 @@ def _sighup(self, signum, frame): log.info('Received SIGHUP') self.restart() - def start(self): + def start(self, background=False): log.info('Starting %s' % SERVER_SOFTWARE) - # Set up our shutdown signals - if self.handle_signals: - try: - import signal - signal.signal(signal.SIGTERM, self._sigterm) - signal.signal(signal.SIGUSR1, self._sighup) - except: - log.debug('This platform does not support signals.') - - # Start our worker threads - self._threadpool.start() - - # Start our monitor thread - self._monitor.setDaemon(True) - self._monitor.start() - - # I know that EXPR and A or B is bad but I'm keeping it for Py2.4 - # compatibility. - str_extract = lambda l: (l.addr, l.port, l.secure and '*' or '') - - msg = 'Listening on sockets: ' - msg += ', '.join(['%s:%i%s' % str_extract(l) for l in self.listeners]) - log.info(msg) - - for l in self.listeners: - l.start() - - tp = self._threadpool - dynamic_resize = tp.dynamic_resize - - while not tp.stop_server: - try: - dynamic_resize() + self.startstop_lock.acquire() + + try: + # Set up our shutdown signals + if self.handle_signals: + try: + import signal + signal.signal(signal.SIGTERM, self._sigterm) + signal.signal(signal.SIGUSR1, self._sighup) + except: + log.debug('This platform does not support signals.') + + # Start our worker threads + self._threadpool.start() + + # Start our monitor thread + self._monitor = Monitor(self.monitor_queue, + self.active_queue, + self.timeout, + self._threadpool) + self._monitor.setDaemon(True) + self._monitor.start() + + # I know that EXPR and A or B is bad but I'm keeping it for Py2.4 + # compatibility. + str_extract = lambda l: (l.addr, l.port, l.secure and '*' or '') + + msg = 'Listening on sockets: ' + msg += ', '.join(['%s:%i%s' % str_extract(l) for l in self.listeners]) + log.info(msg) + + for l in self.listeners: + l.start() + + finally: + self.startstop_lock.release() + + if background: + return + + while self._monitor.isAlive(): + try: time.sleep(THREAD_STOP_CHECK_INTERVAL) except KeyboardInterrupt: # Capture a keyboard interrupt when running from a console break except: - if not tp.stop_server: - log.error(str(traceback.format_exc())) + if self._monitor.isAlive(): + log.error(traceback.format_exc()) continue return self.stop() - def stop(self, stoplogging = True): - log.info("Stopping Server") - - # Stop listeners - for l in self.listeners: - l.ready = False - if l.isAlive(): - l.join() - - # Stop Worker threads - self._threadpool.stop() - - # Stop Monitor - self._monitor.stop() - if self._monitor.isAlive(): - self._monitor.join() - - if stoplogging: - logging.shutdown() + def stop(self, stoplogging = False): + log.info('Stopping %s' % SERVER_SOFTWARE) + + self.startstop_lock.acquire() + + try: + # Stop listeners + for l in self.listeners: + l.ready = False + + # Encourage a context switch + time.sleep(0.01) + + for l in self.listeners: + if l.isAlive(): + l.join() + + # Stop Monitor + self._monitor.stop() + if self._monitor.isAlive(): + self._monitor.join() + + # Stop Worker threads + self._threadpool.stop() + + if stoplogging: + logging.shutdown() + msg = "Calling logging.shutdown() is now the responsibility of \ + the application developer. Please update your \ + applications to no longer call rocket.stop(True)" + try: + import warnings + raise warnings.DeprecationWarning(msg) + except ImportError: + raise RuntimeError(msg) + + finally: + self.startstop_lock.release() def restart(self): - self.stop(False) + self.stop() self.start() def CherryPyWSGIServer(bind_addr, wsgi_app, numthreads = 10, @@ -484,49 +807,57 @@ def __init__(self, monitor_queue, active_queue, timeout, + threadpool, *args, **kwargs): Thread.__init__(self, *args, **kwargs) + + self._threadpool = threadpool # Instance Variables self.monitor_queue = monitor_queue self.active_queue = active_queue self.timeout = timeout + self.log = logging.getLogger('Rocket.Monitor') + self.log.addHandler(NullHandler()) + self.connections = set() self.active = False def run(self): - self.name = self.getName() - self.log = logging.getLogger('Rocket.Monitor') - self.log.addHandler(NullHandler()) - self.active = True conn_list = list() list_changed = False + + # We need to make sure the queue is empty before we start + while not self.monitor_queue.empty(): + self.monitor_queue.get() if __debug__: self.log.debug('Entering monitor loop.') # Enter thread main loop while self.active: + # Move the queued connections to the selection pool - while not self.monitor_queue.empty() or not len(self.connections): + while not self.monitor_queue.empty(): if __debug__: self.log.debug('In "receive timed-out connections" loop.') c = self.monitor_queue.get() if c is None: # A non-client is a signal to die if __debug__: self.log.debug('Received a death threat.') - return + self.stop() + break self.log.debug('Received a timed out connection.') if __debug__: assert(c not in self.connections) @@ -541,43 +872,50 @@ self.connections.add(c) list_changed = True # Wait on those connections - self.log.debug('Blocking on connections') if list_changed: conn_list = list(self.connections) list_changed = False try: - readable = select.select(conn_list, - [], - [], - THREAD_STOP_CHECK_INTERVAL)[0] + if len(conn_list): + readable = select.select(conn_list, + [], + [], + THREAD_STOP_CHECK_INTERVAL)[0] + else: + time.sleep(THREAD_STOP_CHECK_INTERVAL) + readable = [] + + if not self.active: + break + + # If we have any readable connections, put them back + for r in readable: + if __debug__: + self.log.debug('Restoring readable connection') + + if IS_JYTHON: + # Jython requires a socket to be in Non-blocking mode in + # order to select on it, but the rest of the code requires + # that it be in blocking mode. + r.setblocking(True) + + r.start_time = time.time() + self.active_queue.put(r) + + self.connections.remove(r) + list_changed = True + except: if self.active: raise else: break - # If we have any readable connections, put them back - for r in readable: - if __debug__: - self.log.debug('Restoring readable connection') - - if IS_JYTHON: - # Jython requires a socket to be in Non-blocking mode in - # order to select on it, but the rest of the code requires - # that it be in blocking mode. - r.setblocking(True) - - r.start_time = time.time() - self.active_queue.put(r) - - self.connections.remove(r) - list_changed = True - # If we have any stale connections, kill them off. if self.timeout: now = time.time() stale = set() for c in self.connections: @@ -595,18 +933,23 @@ try: c.close() finally: del c + + # Dynamically resize the threadpool to adapt to our changing needs. + self._threadpool.dynamic_resize() + def stop(self): self.active = False if __debug__: self.log.debug('Flushing waiting connections') - for c in self.connections: + while self.connections: + c = self.connections.pop() try: c.close() finally: del c @@ -632,10 +975,11 @@ # Import System Modules import logging # Import Package Modules # package imports removed in monolithic build + # Setup Logging log = logging.getLogger('Rocket.Errors.ThreadPool') log.addHandler(NullHandler()) @@ -661,55 +1005,67 @@ self.worker_class = method self.min_threads = min_threads self.max_threads = max_threads self.monitor_queue = monitor_queue self.stop_server = False + self.alive = False # TODO - Optimize this based on some real-world usage data self.grow_threshold = int(max_threads/10) + 2 if not isinstance(app_info, dict): app_info = dict() + if has_futures and app_info.get('futures'): + app_info['executor'] = WSGIExecutor(max([DEFAULTS['MIN_THREADS'], + 2])) + app_info.update(max_threads=max_threads, min_threads=min_threads) + self.min_threads = min_threads self.app_info = app_info self.threads = set() - for x in range(min_threads): - worker = self.worker_class(app_info, - self.active_queue, - self.monitor_queue) - self.threads.add(worker) def start(self): self.stop_server = False if __debug__: log.debug("Starting threads.") - for thread in self.threads: - thread.setDaemon(True) - thread.start() + self.grow(self.min_threads) + + self.alive = True def stop(self): + self.alive = False + if __debug__: log.debug("Stopping threads.") self.stop_server = True # Prompt the threads to die - for t in self.threads: - self.active_queue.put(None) + self.shrink(len(self.threads)) + + # Stop futures initially + if has_futures and self.app_info.get('futures'): + if __debug__: + log.debug("Future executor is present. Python will not " + "exit until all jobs have finished.") + self.app_info['executor'].shutdown(wait=False) # Give them the gun - for t in self.threads: - t.kill() + #active_threads = [t for t in self.threads if t.isAlive()] + #while active_threads: + # t = active_threads.pop() + # t.kill() # Wait until they pull the trigger for t in self.threads: - t.join() + if t.isAlive(): + t.join() # Clean up the mess self.bring_out_your_dead() def bring_out_your_dead(self): @@ -731,11 +1087,12 @@ return if not amount: amount = self.max_threads - amount = min([amount, self.max_threads - len(self.threads)]) + if self.alive: + amount = min([amount, self.max_threads - len(self.threads)]) if __debug__: log.debug("Growing by %i." % amount) for x in range(amount): @@ -782,11 +1139,11 @@ import re import sys import socket import logging import traceback -#from wsgiref.headers import Headers +from wsgiref.headers import Headers from threading import Thread from datetime import datetime try: from urllib import unquote @@ -835,130 +1192,10 @@ %s ''' if IS_JYTHON: HTTP_METHODS = set(['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT']) -### -# The Headers and FileWrapper classes are ripped straight from the Python -# Standard Library. I've removed some docstrings and integrated my BUF_SIZE. -# See the Python License here: http://docs.python.org/license.html -### - -# Regular expression that matches `special' characters in parameters, the -# existance of which force quoting of the parameter value. -import re -_tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') - -def _formatparam(param, value=None, quote=1): - """Convenience function to format and return a key=value pair. - - This will quote the value if needed or if quote is true. - """ - if value is not None and len(value) > 0: - if quote or _tspecials.search(value): - value = value.replace('\\', '\\\\').replace('"', r'\"') - return '%s="%s"' % (param, value) - else: - return '%s=%s' % (param, value) - else: - return param - -class Headers: - def __init__(self,headers): - if type(headers) is not type([]): - raise TypeError("Headers must be a list of name/value tuples") - self._headers = headers - - def __len__(self): - return len(self._headers) - - def __setitem__(self, name, val): - del self[name] - self._headers.append((name, val)) - - def __delitem__(self,name): - name = name.lower() - self._headers[:] = [kv for kv in self._headers if kv[0].lower() != name] - - def __getitem__(self,name): - return self.get(name) - - def has_key(self, name): - return self.get(name) is not None - - __contains__ = has_key - - def get_all(self, name): - name = name.lower() - return [kv[1] for kv in self._headers if kv[0].lower()==name] - - def get(self,name,default=None): - name = name.lower() - for k,v in self._headers: - if k.lower()==name: - return v - return default - - def keys(self): - return [k for k, v in self._headers] - - def values(self): - return [v for k, v in self._headers] - - def items(self): - return self._headers[:] - - def __repr__(self): - return "Headers(%r)" % self._headers - - def __str__(self): - return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['','']) - - def setdefault(self,name,value): - result = self.get(name) - if result is None: - self._headers.append((name,value)) - return value - else: - return result - - - def add_header(self, _name, _value, **_params): - parts = [] - if _value is not None: - parts.append(_value) - for k, v in _params.items(): - if v is None: - parts.append(k.replace('_', '-')) - else: - parts.append(_formatparam(k.replace('_', '-'), v)) - self._headers.append((_name, "; ".join(parts))) - -class FileWrapper: - """Wrapper to convert file-like objects to iterables""" - - def __init__(self, filelike, blksize=BUF_SIZE): - self.filelike = filelike - self.blksize = blksize - if hasattr(filelike,'close'): - self.close = filelike.close - - def __getitem__(self,key): - data = self.filelike.read(self.blksize) - if data: - return data - raise IndexError - - def __iter__(self): - return self - - def next(self): - data = self.filelike.read(self.blksize) - if data: - return data - raise StopIteration - class Worker(Thread): """The Worker class is a base class responsible for receiving connections and (a subclass) will run an application to process the the connection """ def __init__(self, @@ -976,10 +1213,11 @@ self.monitor_queue = monitor_queue self.size = 0 self.status = "200 OK" self.closeConnection = True + self.request_line = "" # Request Log self.req_log = logging.getLogger('Rocket.Requests') self.req_log.addHandler(NullHandler()) @@ -1107,18 +1345,18 @@ except socket.error: self.closeConnection = True self.err_log.error('Tried to send "%s" to client but received socket' ' error' % status) - def kill(self): - if self.isAlive() and hasattr(self, 'conn'): - try: - self.conn.shutdown(socket.SHUT_RDWR) - except socket.error: - info = sys.exc_info() - if info[1].args[0] != socket.EBADF: - self.err_log.debug('Error on shutdown: '+str(info)) + #def kill(self): + # if self.isAlive() and hasattr(self, 'conn'): + # try: + # self.conn.shutdown(socket.SHUT_RDWR) + # except socket.error: + # info = sys.exc_info() + # if info[1].args[0] != socket.EBADF: + # self.err_log.debug('Error on shutdown: '+str(info)) def read_request_line(self, sock_file): self.request_line = '' try: # Grab the request line @@ -1207,38 +1445,42 @@ host=host) return req def read_headers(self, sock_file): - headers = dict() - l = sock_file.readline() - - lname = None - lval = None - while True: - if PY3K: - try: - l = str(l, 'ISO-8859-1') - except UnicodeDecodeError: - self.err_log.warning('Client sent invalid header: ' + repr(l)) - - if l == '\r\n': - break - - if l[0] in ' \t' and lname: - # Some headers take more than one line - lval += ',' + l.strip() - else: - # HTTP header values are latin-1 encoded - l = l.split(':', 1) - # HTTP header names are us-ascii encoded - - lname = l[0].strip().upper().replace('-', '_') - lval = l[-1].strip() - headers[str(lname)] = str(lval) - - l = sock_file.readline() + try: + headers = dict() + l = sock_file.readline() + + lname = None + lval = None + while True: + if PY3K: + try: + l = str(l, 'ISO-8859-1') + except UnicodeDecodeError: + self.err_log.warning('Client sent invalid header: ' + repr(l)) + + if l == '\r\n': + break + + if l[0] in ' \t' and lname: + # Some headers take more than one line + lval += ',' + l.strip() + else: + # HTTP header values are latin-1 encoded + l = l.split(':', 1) + # HTTP header names are us-ascii encoded + + lname = l[0].strip().upper().replace('-', '_') + lval = l[-1].strip() + headers[str(lname)] = str(lval) + + l = sock_file.readline() + except socket.timeout: + raise SocketTimeout("Socket timed out before request.") + return headers class SocketTimeout(Exception): "Exception for when a socket times out between requests." pass @@ -1298,26 +1540,222 @@ def readlines(self): yield self.readline() def get_method(method): - methods = dict(wsgi=WSGIWorker) + + methods = dict(wsgi=WSGIWorker, + fs=FileSystemWorker) return methods[method.lower()] # Monolithic build...end of module: rocket\worker.py # Monolithic build...start of module: rocket\methods\__init__.py # Monolithic build...end of module: rocket\methods\__init__.py +# Monolithic build...start of module: rocket\methods\fs.py + +# Import System Modules +import os +import time +import mimetypes +from email.utils import formatdate +from wsgiref.headers import Headers +from wsgiref.util import FileWrapper +# Import Package Modules +# package imports removed in monolithic build + + +# Define Constants +CHUNK_SIZE = 2**16 # 64 Kilobyte chunks +HEADER_RESPONSE = '''HTTP/1.1 %s\r\n%s''' +INDEX_HEADER = '''\ + +Directory Index: %(path)s + + +

    Directory Index: %(path)s

    +
    + +''' +INDEX_ROW = '''''' +INDEX_FOOTER = '''
    Directories
    \r\n''' + +class LimitingFileWrapper(FileWrapper): + def __init__(self, limit=None, *args, **kwargs): + self.limit = limit + FileWrapper.__init__(self, *args, **kwargs) + + def read(self, amt): + if amt > self.limit: + amt = self.limit + self.limit -= amt + return FileWrapper.read(self, amt) + +class FileSystemWorker(Worker): + def __init__(self, *args, **kwargs): + """Builds some instance variables that will last the life of the + thread.""" + + Worker.__init__(self, *args, **kwargs) + + self.root = os.path.abspath(self.app_info['document_root']) + self.display_index = self.app_info['display_index'] + + def serve_file(self, filepath, headers): + filestat = os.stat(filepath) + self.size = filestat.st_size + modtime = time.strftime("%a, %d %b %Y %H:%M:%S GMT", + time.gmtime(filestat.st_mtime)) + self.headers.add_header('Last-Modified', modtime) + if headers.get('if_modified_since') == modtime: + # The browser cache is up-to-date, send a 304. + self.status = "304 Not Modified" + self.data = [] + return + + ct = mimetypes.guess_type(filepath)[0] + self.content_type = ct if ct else 'text/plain' + try: + f = open(filepath, 'rb') + self.headers['Pragma'] = 'cache' + self.headers['Cache-Control'] = 'private' + self.headers['Content-Length'] = str(self.size) + if self.etag: + self.headers.add_header('Etag', self.etag) + if self.expires: + self.headers.add_header('Expires', self.expires) + + try: + # Implement 206 partial file support. + start, end = headers['range'].split('-') + start = 0 if not start.isdigit() else int(start) + end = self.size if not end.isdigit() else int(end) + if self.size < end or start < 0: + self.status = "214 Unsatisfiable Range Requested" + self.data = FileWrapper(f, CHUNK_SIZE) + else: + f.seek(start) + self.data = LimitingFileWrapper(f, CHUNK_SIZE, limit=end) + self.status = "206 Partial Content" + except: + self.data = FileWrapper(f, CHUNK_SIZE) + except IOError: + self.status = "403 Forbidden" + + def serve_dir(self, pth, rpth): + def rel_path(path): + return os.path.normpath(path[len(self.root):] if path.startswith(self.root) else path) + + if not self.display_index: + self.status = '404 File Not Found' + return b('') + else: + self.content_type = 'text/html' + + dir_contents = [os.path.join(pth, x) for x in os.listdir(os.path.normpath(pth))] + dir_contents.sort() + + dirs = [rel_path(x)+'/' for x in dir_contents if os.path.isdir(x)] + files = [rel_path(x) for x in dir_contents if os.path.isfile(x)] + + self.data = [INDEX_HEADER % dict(path='/'+rpth)] + if rpth: + self.data += [INDEX_ROW % dict(name='(parent directory)', cls='dir parent', link='/'.join(rpth[:-1].split('/')[:-1]))] + self.data += [INDEX_ROW % dict(name=os.path.basename(x[:-1]), link=os.path.join(rpth, os.path.basename(x[:-1])).replace('\\', '/'), cls='dir') for x in dirs] + self.data += ['Files'] + self.data += [INDEX_ROW % dict(name=os.path.basename(x), link=os.path.join(rpth, os.path.basename(x)).replace('\\', '/'), cls='file') for x in files] + self.data += [INDEX_FOOTER] + self.headers['Content-Length'] = self.size = str(sum([len(x) for x in self.data])) + self.status = '200 OK' + + def run_app(self, conn): + self.status = "200 OK" + self.size = 0 + self.expires = None + self.etag = None + self.content_type = 'text/plain' + self.content_length = None + + if __debug__: + self.err_log.debug('Getting sock_file') + + # Build our file-like object + sock_file = conn.makefile('rb',BUF_SIZE) + request = self.read_request_line(sock_file) + if request['method'].upper() not in ('GET', ): + self.status = "501 Not Implemented" + + try: + # Get our file path + headers = dict([(str(k.lower()), v) for k, v in self.read_headers(sock_file).items()]) + rpath = request.get('path', '').lstrip('/') + filepath = os.path.join(self.root, rpath) + filepath = os.path.abspath(filepath) + if __debug__: + self.err_log.debug('Request for path: %s' % filepath) + + self.closeConnection = headers.get('connection', 'close').lower() == 'close' + self.headers = Headers([('Date', formatdate(usegmt=True)), + ('Server', HTTP_SERVER_SOFTWARE), + ('Connection', headers.get('connection', 'close')), + ]) + + if not filepath.lower().startswith(self.root.lower()): + # File must be within our root directory + self.status = "400 Bad Request" + self.closeConnection = True + elif not os.path.exists(filepath): + self.status = "404 File Not Found" + self.closeConnection = True + elif os.path.isdir(filepath): + self.serve_dir(filepath, rpath) + elif os.path.isfile(filepath): + self.serve_file(filepath, headers) + else: + # It exists but it's not a file or a directory???? + # What is it then? + self.status = "501 Not Implemented" + self.closeConnection = True + + h = self.headers + statcode, statstr = self.status.split(' ', 1) + statcode = int(statcode) + if statcode >= 400: + h.add_header('Content-Type', self.content_type) + self.data = [statstr] + + # Build our output headers + header_data = HEADER_RESPONSE % (self.status, str(h)) + + # Send the headers + if __debug__: + self.err_log.debug('Sending Headers: %s' % repr(header_data)) + self.conn.sendall(b(header_data)) + + for data in self.data: + self.conn.sendall(b(data)) + + if hasattr(self.data, 'close'): + self.data.close() + + finally: + if __debug__: + self.err_log.debug('Finally closing sock_file') + sock_file.close() + +# Monolithic build...end of module: rocket\methods\fs.py # Monolithic build...start of module: rocket\methods\wsgi.py # Import System Modules import sys import socket -#from wsgiref.headers import Headers -#from wsgiref.util import FileWrapper +from wsgiref.headers import Headers +from wsgiref.util import FileWrapper + # Import Package Modules # package imports removed in monolithic build + if PY3K: from email.utils import formatdate else: @@ -1348,16 +1786,22 @@ multithreaded = False self.base_environ = dict({'SERVER_SOFTWARE': self.app_info['server_software'], 'wsgi.multithread': multithreaded, }) self.base_environ.update(BASE_ENV) + # Grab our application self.app = self.app_info.get('wsgi_app') if not hasattr(self.app, "__call__"): raise TypeError("The wsgi_app specified (%s) is not a valid WSGI application." % repr(self.app)) + # Enable futures + if has_futures and self.app_info.get('futures'): + executor = self.app_info['executor'] + self.base_environ.update({"wsgiorg.executor": executor, + "wsgiorg.futures": executor.futures}) def build_environ(self, sock_file, conn): """ Build the execution environment. """ # Grab the request line request = self.read_request_line(sock_file) @@ -1390,10 +1834,18 @@ environ['wsgi.url_scheme'] = 'https' environ['HTTPS'] = 'on' else: environ['wsgi.url_scheme'] = 'http' + if conn.ssl: + try: + peercert = conn.socket.getpeercert(binary_form=True) + environ['SSL_CLIENT_RAW_CERT'] = \ + peercert and ssl.DER_cert_to_PEM_cert(peercert) + except Exception,e: + print e + if environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked': environ['wsgi.input'] = ChunkedReader(sock_file) else: environ['wsgi.input'] = sock_file @@ -1522,11 +1974,14 @@ if __debug__: self.err_log.debug('Getting sock_file') # Build our file-like object - sock_file = conn.makefile('rb',BUF_SIZE) + if PY3K: + sock_file = conn.makefile(mode='rb', buffering=BUF_SIZE) + else: + sock_file = conn.makefile(BUF_SIZE) try: # Read the headers and build our WSGI environment self.environ = environ = self.build_environ(sock_file, conn) @@ -1585,15 +2040,11 @@ static_folder = os.path.join(os.getcwd(),static_folder) path = os.path.join(static_folder, environ['PATH_INFO'][1:] or 'index.html') type = types.get(path.split('.')[-1],'text') if os.path.exists(path): try: - pathfile = open(path,'rb') - try: - data = pathfile.read() - finally: - pathfile.close() + data = open(path,'rb').read() start_response('200 OK', [('Content-Type', type)]) except IOError: start_response('404 NOT FOUND', []) data = '404 NOT FOUND' else: ADDED gluon/rocket.pyc Index: gluon/rocket.pyc ================================================================== --- gluon/rocket.pyc +++ gluon/rocket.pyc cannot compute difference between binary files Index: gluon/sanitizer.py ================================================================== --- gluon/sanitizer.py +++ gluon/sanitizer.py @@ -220,6 +220,8 @@ }, escape=True): if not isinstance(text, str): return str(text) return XssCleaner(permitted_tags=permitted_tags, allowed_attributes=allowed_attributes).strip(text, escape) + + ADDED gluon/sanitizer.pyc Index: gluon/sanitizer.pyc ================================================================== --- gluon/sanitizer.pyc +++ gluon/sanitizer.pyc cannot compute difference between binary files ADDED gluon/scheduler.py Index: gluon/scheduler.py ================================================================== --- gluon/scheduler.py +++ gluon/scheduler.py @@ -0,0 +1,547 @@ +#### WORK IN PROGRESS... NOT SUPPOSED TO WORK YET + +USAGE = """ +## Example + +For any existing app + +Create File: app/models/scheduler.py ====== +from gluon.scheduler import Scheduler + +def demo1(*args,**vars): + print 'you passed args=%s and vars=%s' % (args, vars) + return 'done!' + +def demo2(): + 1/0 + +scheduler = Scheduler(db,dict(demo1=demo1,demo2=demo2)) +## run worker nodes with: + + cd web2py + python gluon/scheduler.py -u sqlite://storage.sqlite \ + -f applications/myapp/databases/ \ + -t mytasks.py +(-h for info) +python scheduler.py -h + +## schedule jobs using +http://127.0.0.1:8000/scheduler/appadmin/insert/db/scheduler_task + +## monitor scheduled jobs +http://127.0.0.1:8000/scheduler/appadmin/select/db?query=db.scheduler_task.id>0 + +## view completed jobs +http://127.0.0.1:8000/scheduler/appadmin/select/db?query=db.scheduler_run.id>0 + +## view workers +http://127.0.0.1:8000/scheduler/appadmin/select/db?query=db.scheduler_worker.id>0 + +## Comments +""" + +import os +import time +import multiprocessing +import sys +import cStringIO +import threading +import traceback +import signal +import socket +import datetime +import logging +import optparse + +try: + from gluon.contrib.simplejson import loads, dumps +except: + from simplejson import loads, dumps + +if 'WEB2PY_PATH' in os.environ: + sys.path.append(os.environ['WEB2PY_PATH']) +else: + os.environ['WEB2PY_PATH'] = os.getcwd() + +from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET +from gluon.utils import web2py_uuid + +QUEUED = 'QUEUED' +ASSIGNED = 'ASSIGNED' +RUNNING = 'RUNNING' +COMPLETED = 'COMPLETED' +FAILED = 'FAILED' +TIMEOUT = 'TIMEOUT' +STOPPED = 'STOPPED' +ACTIVE = 'ACTIVE' +INACTIVE = 'INACTIVE' +DISABLED = 'DISABLED' +SECONDS = 1 +HEARTBEAT = 3*SECONDS + +class Task(object): + def __init__(self,app,function,timeout,args='[]',vars='{}',**kwargs): + logging.debug(' new task allocated: %s.%s' % (app,function)) + self.app = app + self.function = function + self.timeout = timeout + self.args = args # json + self.vars = vars # json + self.__dict__.update(kwargs) + def __str__(self): + return '' % self.function + +class TaskReport(object): + def __init__(self,status,result=None,output=None,tb=None): + logging.debug(' new task report: %s' % status) + if tb: + logging.debug(' traceback: %s' % tb) + else: + logging.debug(' result: %s' % result) + self.status = status + self.result = result + self.output = output + self.tb = tb + def __str__(self): + return '' % self.status + +def demo_function(*argv,**kwargs): + """ test function """ + for i in range(argv[0]): + print 'click',i + time.sleep(1) + return 'done' + +#the two functions below deal with simplejson decoding as unicode, esp for the dict decode +#and subsequent usage as function Keyword arguments unicode variable names won't work! +#borrowed from http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-unicode-ones-from-json-in-python +def _decode_list(lst): + newlist = [] + for i in lst: + if isinstance(i, unicode): + i = i.encode('utf-8') + elif isinstance(i, list): + i = _decode_list(i) + newlist.append(i) + return newlist + +def _decode_dict(dct): + newdict = {} + for k, v in dct.iteritems(): + if isinstance(k, unicode): + k = k.encode('utf-8') + if isinstance(v, unicode): + v = v.encode('utf-8') + elif isinstance(v, list): + v = _decode_list(v) + newdict[k] = v + return newdict + +def executor(queue,task): + """ the background process """ + logging.debug(' task started') + stdout, sys.stdout = sys.stdout, cStringIO.StringIO() + try: + if task.app: + os.chdir(os.environ['WEB2PY_PATH']) + from gluon.shell import env + from gluon.dal import BaseAdapter + from gluon import current + level = logging.getLogger().getEffectiveLevel() + logging.getLogger().setLevel(logging.WARN) + _env = env(task.app,import_models=True) + logging.getLogger().setLevel(level) + scheduler = current._scheduler + scheduler_tasks = current._scheduler.tasks + _function = scheduler_tasks[task.function] + globals().update(_env) + args = loads(task.args) + vars = loads(task.vars, object_hook=_decode_dict) + result = dumps(_function(*args,**vars)) + else: + ### for testing purpose only + result = eval(task.function)(*loads(task.args, list_hook),**loads(task.vars, object_hook=_decode_dict)) + stdout, sys.stdout = sys.stdout, stdout + queue.put(TaskReport(COMPLETED, result,stdout.getvalue())) + except BaseException,e: + sys.stdout = stdout + tb = traceback.format_exc() + queue.put(TaskReport(FAILED,tb=tb)) + +class MetaScheduler(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.process = None # the backround process + self.have_heartbeat = True # set to False to kill + def async(self,task): + """ + starts the background process and returns: + ('ok',result,output) + ('error',exception,None) + ('timeout',None,None) + ('terminated',None,None) + """ + queue = multiprocessing.Queue(maxsize=1) + p = multiprocessing.Process(target=executor,args=(queue,task)) + self.process = p + logging.debug(' task starting') + p.start() + try: + p.join(task.timeout) + except: + p.terminate() + p.join() + self.have_heartbeat = False + logging.debug(' task stopped') + return TaskReport(STOPPED) + if p.is_alive(): + p.terminate() + p.join() + logging.debug(' task timeout') + return TaskReport(TIMEOUT) + elif queue.empty(): + self.have_heartbeat = False + logging.debug(' task stopped') + return TaskReport(STOPPED) + else: + logging.debug(' task completed or failed') + return queue.get() + + def die(self): + logging.info('die!') + self.have_heartbeat = False + self.terminate_process() + + def terminate_process(self): + try: + self.process.terminate() + except: + pass # no process to terminate + + def run(self): + """ the thread that sends heartbeat """ + counter = 0 + while self.have_heartbeat: + self.send_heartbeat(counter) + counter += 1 + + def start_heartbeats(self): + self.start() + + def send_heartbeat(self,counter): + print 'thum' + time.sleep(1) + + def pop_task(self): + return Task( + app = None, + function = 'demo_function', + timeout = 7, + args = '[2]', + vars = '{}') + + def report_task(self,task,task_report): + print 'reporting task' + pass + + def sleep(self): + pass + + def loop(self): + try: + self.start_heartbeats() + while True and self.have_heartbeat: + logging.debug('looping...') + task = self.pop_task() + if task: + self.report_task(task,self.async(task)) + else: + logging.debug('sleeping...') + self.sleep() + except KeyboardInterrupt: + self.die() + + +TASK_STATUS = (QUEUED, RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED) +RUN_STATUS = (RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED) +WORKER_STATUS = (ACTIVE,INACTIVE,DISABLED) + +class TYPE(object): + """ + validator that check whether field is valid json and validate its type + """ + + def __init__(self,myclass=list,parse=False): + self.myclass = myclass + self.parse=parse + + def __call__(self,value): + from gluon import current + try: + obj = loads(value) + except: + return (value,current.T('invalid json')) + else: + if isinstance(obj,self.myclass): + if self.parse: + return (obj,None) + else: + return (value,None) + else: + return (value,current.T('Not of type: %s') % self.myclass) + +class Scheduler(MetaScheduler): + def __init__(self,db,tasks={},migrate=True, + worker_name=None,group_names=None,heartbeat=HEARTBEAT): + + MetaScheduler.__init__(self) + + self.db = db + self.db_thread = None + self.tasks = tasks + self.group_names = group_names or ['main'] + self.heartbeat = heartbeat + self.worker_name = worker_name or socket.gethostname()+'#'+str(web2py_uuid()) + + from gluon import current + current._scheduler = self + + self.define_tables(db,migrate=migrate) + + def define_tables(self,db,migrate): + from gluon import current + logging.debug('defining tables (migrate=%s)' % migrate) + now = datetime.datetime.now() + db.define_table( + 'scheduler_task', + Field('application_name',requires=IS_NOT_EMPTY(), + default=None,writable=False), + Field('task_name',requires=IS_NOT_EMPTY()), + Field('group_name',default='main',writable=False), + Field('status',requires=IS_IN_SET(TASK_STATUS), + default=QUEUED,writable=False), + Field('function_name', + requires=IS_IN_SET(sorted(self.tasks.keys()))), + Field('args','text',default='[]',requires=TYPE(list)), + Field('vars','text',default='{}',requires=TYPE(dict)), + Field('enabled','boolean',default=True), + Field('start_time','datetime',default=now), + Field('next_run_time','datetime',default=now), + Field('stop_time','datetime',default=now+datetime.timedelta(days=1)), + Field('repeats','integer',default=1,comment="0=unlimted"), + Field('period','integer',default=60,comment='seconds'), + Field('timeout','integer',default=60,comment='seconds'), + Field('times_run','integer',default=0,writable=False), + Field('last_run_time','datetime',writable=False,readable=False), + Field('assigned_worker_name',default='',writable=False), + migrate=migrate,format='%(task_name)s') + if hasattr(current,'request'): + db.scheduler_task.application_name.default=current.request.application + + db.define_table( + 'scheduler_run', + Field('scheduler_task','reference scheduler_task'), + Field('status',requires=IS_IN_SET(RUN_STATUS)), + Field('start_time','datetime'), + Field('stop_time','datetime'), + Field('output','text'), + Field('result','text'), + Field('traceback','text'), + Field('worker_name',default=self.worker_name), + migrate=migrate) + + db.define_table( + 'scheduler_worker', + Field('worker_name'), + Field('first_heartbeat','datetime'), + Field('last_heartbeat','datetime'), + Field('status',requires=IS_IN_SET(WORKER_STATUS)), + migrate=migrate) + db.commit() + + def loop(self,worker_name=None): + MetaScheduler.loop(self) + + def pop_task(self): + now = datetime.datetime.now() + db, ts = self.db, self.db.scheduler_task + try: + logging.debug(' grabbing all queued tasks') + all_available = db(ts.status.belongs((QUEUED,RUNNING)))\ + ((ts.times_runnow)\ + (ts.next_run_time<=now)\ + (ts.enabled==True)\ + (ts.assigned_worker_name.belongs((None,'',self.worker_name))) #None? + number_grabbed = all_available.update( + assigned_worker_name=self.worker_name,status=ASSIGNED) + db.commit() + except: + db.rollback() + logging.debug(' grabbed %s tasks' % number_grabbed) + if number_grabbed: + grabbed = db(ts.assigned_worker_name==self.worker_name)\ + (ts.status==ASSIGNED) + task = grabbed.select(limitby=(0,1), orderby=ts.next_run_time).first() + + logging.debug(' releasing all but one (running)') + if task: + task.update_record(status=RUNNING,last_run_time=now) + grabbed.update(assigned_worker_name='',status=QUEUED) + db.commit() + else: + return None + next_run_time = task.last_run_time + datetime.timedelta(seconds=task.period) + times_run = task.times_run + 1 + if times_run < task.repeats or task.repeats==0: + run_again = True + else: + run_again = False + logging.debug(' new scheduler_run record') + while True: + try: + run_id = db.scheduler_run.insert( + scheduler_task = task.id, + status=RUNNING, + start_time=now, + worker_name=self.worker_name) + db.commit() + break + except: + db.rollback + logging.info('new task %(id)s "%(task_name)s" %(application_name)s.%(function_name)s' % task) + return Task( + app = task.application_name, + function = task.function_name, + timeout = task.timeout, + args = task.args, #in json + vars = task.vars, #in json + task_id = task.id, + run_id = run_id, + run_again = run_again, + next_run_time=next_run_time, + times_run = times_run) + + def report_task(self,task,task_report): + logging.debug(' recording task report in db (%s)' % task_report.status) + db = self.db + db(db.scheduler_run.id==task.run_id).update( + status = task_report.status, + stop_time = datetime.datetime.now(), + result = task_report.result, + output = task_report.output, + traceback = task_report.tb) + if task_report.status == COMPLETED: + d = dict(status = task.run_again and QUEUED or COMPLETED, + next_run_time = task.next_run_time, + times_run = task.times_run, + assigned_worker_name = '') + else: + d = dict( + assigned_worker_name = '', + status = {'FAILED':'FAILED', + 'TIMEOUT':'TIMEOUT', + 'STOPPED':'QUEUED'}[task_report.status]) + db(db.scheduler_task.id==task.task_id)\ + (db.scheduler_task.status==RUNNING).update(**d) + db.commit() + logging.info('task completed (%s)' % task_report.status) + + def send_heartbeat(self,counter): + if not self.db_thread: + logging.debug('thread building own DAL object') + self.db_thread = DAL(self.db._uri,folder = self.db._adapter.folder) + self.define_tables(self.db_thread,migrate=False) + try: + db = self.db_thread + sw, st = db.scheduler_worker, db.scheduler_task + now = datetime.datetime.now() + expiration = now-datetime.timedelta(seconds=self.heartbeat*3) + # record heartbeat + logging.debug('........recording heartbeat') + if not db(sw.worker_name==self.worker_name)\ + .update(last_heartbeat = now, status = ACTIVE): + sw.insert(status = ACTIVE,worker_name = self.worker_name, + first_heartbeat = now,last_heartbeat = now) + if counter % 10 == 0: + # deallocate jobs assigned to inactive workers and requeue them + logging.debug(' freeing workers that have not sent heartbeat') + inactive_workers = db(sw.last_heartbeat' % encoding) + str(xml_rec(value,key)) def json(value,default=custom_json): - return simplejson.dumps(value,default=default) + return json_parser.dumps(value,default=default) def csv(value): return '' @@ -73,6 +80,8 @@ )\ for entry in feed['entries'] ] ) return rss2.dumps(rss) + + ADDED gluon/serializers.pyc Index: gluon/serializers.pyc ================================================================== --- gluon/serializers.pyc +++ gluon/serializers.pyc cannot compute difference between binary files Index: gluon/settings.py ================================================================== --- gluon/settings.py +++ gluon/settings.py @@ -6,6 +6,8 @@ from storage import Storage global_settings = Storage() settings = global_settings # legacy compatibility + + ADDED gluon/settings.pyc Index: gluon/settings.pyc ================================================================== --- gluon/settings.pyc +++ gluon/settings.pyc cannot compute difference between binary files Index: gluon/shell.py ================================================================== --- gluon/shell.py +++ gluon/shell.py @@ -15,19 +15,20 @@ import logging import types import re import optparse import glob - +import traceback import fileutils import settings from utils import web2py_uuid from compileapp import build_environment, read_pyc, run_models_in from restricted import RestrictedError from globals import Request, Response, Session from storage import Storage from admin import w2p_unpack +from dal import BaseAdapter logger = logging.getLogger("web2py") def exec_environment( @@ -48,13 +49,13 @@ A Storage dictionary containing the resulting environment is returned. The working directory must be web2py root -- this is the web2py default. """ - if request==None: request = Request() - if response==None: response = Response() - if session==None: session = Session() + if request is None: request = Request() + if response is None: response = Response() + if session is None: session = Session() if request.folder is None: mo = re.match(r'(|.*/)applications/(?P[^/]+)', pyfile) if mo: appname = mo.group('appname') @@ -148,11 +149,12 @@ def run( appname, plain=False, import_models=False, startfile=None, - bpython=False + bpython=False, + python_code=False ): """ Start interactive shell or run Python script (startfile) in web2py controller environment. appname is formatted like: @@ -200,12 +202,22 @@ exec ('print %s()' % f, _env) elif startfile: exec_pythonrc() try: execfile(startfile, _env) - except RestrictedError, e: - print e.traceback + if import_models: BaseAdapter.close_all_instances('commit') + except Exception, e: + print traceback.format_exc() + if import_models: BaseAdapter.close_all_instances('rollback') + elif python_code: + exec_pythonrc() + try: + exec(python_code, _env) + if import_models: BaseAdapter.close_all_instances('commit') + except Exception, e: + print traceback.format_exc() + if import_models: BaseAdapter.close_all_instances('rollback') else: if not plain: if bpython: try: import bpython @@ -215,16 +227,23 @@ logger.warning( 'import bpython error; trying ipython...') else: try: import IPython - # following 2 lines fix a problem with IPython; thanks Michael Toomim - if '__builtins__' in _env: - del _env['__builtins__'] - shell = IPython.Shell.IPShell(argv=[], user_ns=_env) - shell.mainloop() - return + if IPython.__version__ >= '0.11': + from IPython.frontend.terminal.embed import InteractiveShellEmbed + shell = InteractiveShellEmbed(user_ns=_env) + shell() + return + else: + # following 2 lines fix a problem with + # IPython; thanks Michael Toomim + if '__builtins__' in _env: + del _env['__builtins__'] + shell = IPython.Shell.IPShell(argv=[],user_ns=_env) + shell.mainloop() + return except: logger.warning( 'import IPython error; use default python shell') try: import readline @@ -395,6 +414,8 @@ run(options.shell, options.plain, startfile=startfile, bpython=options.bpython) if __name__ == '__main__': execute_from_command_line() + + ADDED gluon/shell.pyc Index: gluon/shell.pyc ================================================================== --- gluon/shell.pyc +++ gluon/shell.pyc cannot compute difference between binary files Index: gluon/sql.py ================================================================== --- gluon/sql.py +++ gluon/sql.py @@ -1,6 +1,8 @@ # this file exists for backward compatibility __all__ = ['DAL','Field','drivers'] from dal import DAL, Field, Table, Query, Set, Expression, Row, Rows, drivers, BaseAdapter, SQLField, SQLTable, SQLXorable, SQLQuery, SQLSet, SQLRows, SQLStorage, SQLDB, GQLDB, SQLALL, SQLCustomType + + Index: gluon/sqlhtml.py ================================================================== --- gluon/sqlhtml.py +++ gluon/sqlhtml.py @@ -13,23 +13,22 @@ - form_factory: provides a SQLFORM for an non-db backed table """ from http import HTTP -from html import XML, SPAN, TAG, A, DIV, UL, LI, TEXTAREA, BR, IMG, SCRIPT +from html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT from html import FORM, INPUT, LABEL, OPTION, SELECT from html import TABLE, THEAD, TBODY, TR, TD, TH -from html import URL as Url -from dal import DAL, Table, Row, CALLABLETYPES +from html import URL +from dal import DAL, Table, Row, CALLABLETYPES, smart_query from storage import Storage from utils import md5_hash from validators import IS_EMPTY_OR import urllib import re import cStringIO - table_field = re.compile('[\w_]+\.[\w_]+') widget_class = re.compile('^\w*') def represent(field,value,record): @@ -109,11 +108,11 @@ see also: :meth:`FormWidget.widget` """ default = dict( _type = 'text', - value = (value!=None and str(value)) or '', + value = (not value is None and str(value)) or '', ) attr = StringWidget._attributes(field, default, **attributes) return INPUT(**attr) @@ -292,11 +291,12 @@ generates a TABLE tag, including INPUT radios (only 1 option allowed) see also: :meth:`FormWidget.widget` """ - attr = OptionsWidget._attributes(field, {}, **attributes) + attr = RadioWidget._attributes(field, {}, **attributes) + attr['_class'] = attr.get('_class','web2py_radiowidget') requires = field.requires if not isinstance(requires, (list, tuple)): requires = [requires] if requires: @@ -303,32 +303,44 @@ if hasattr(requires[0], 'options'): options = requires[0].options() else: raise SyntaxError, 'widget cannot determine options of %s' \ % field - options = [(k, v) for k, v in options if str(v)] opts = [] cols = attributes.get('cols',1) totals = len(options) mods = totals%cols rows = totals/cols if mods: rows += 1 + #widget style + wrappers = dict( + table=(TABLE,TR,TD), + ul=(DIV,UL,LI), + divs=(CAT,DIV,DIV) + ) + parent, child, inner = wrappers[attributes.get('style','table')] + for r_index in range(rows): tds = [] for k, v in options[r_index*cols:(r_index+1)*cols]: - tds.append(TD(INPUT(_type='radio', _name=field.name, - requires=attr.get('requires',None), - hideerror=True, _value=k, - value=value), v)) - opts.append(TR(tds)) + checked={'_checked':'checked'} if k==value else {} + tds.append(inner(INPUT(_type='radio', + _id='%s%s' % (field.name,k), + _name=field.name, + requires=attr.get('requires',None), + hideerror=True, _value=k, + value=value, + **checked), + LABEL(v,_for='%s%s' % (field.name,k)))) + opts.append(child(tds)) if opts: opts[-1][0][0]['hideerror'] = False - return TABLE(*opts, **attr) + return parent(*opts, **attr) class CheckboxesWidget(OptionsWidget): @staticmethod @@ -343,11 +355,12 @@ if isinstance(value, (list, tuple)): values = [str(v) for v in value] else: values = [str(value)] - attr = OptionsWidget._attributes(field, {}, **attributes) + attr = CheckboxesWidget._attributes(field, {}, **attributes) + attr['_class'] = attr.get('_class','web2py_checkboxeswidget') requires = field.requires if not isinstance(requires, (list, tuple)): requires = [requires] if requires: @@ -364,26 +377,37 @@ mods = totals % cols rows = totals / cols if mods: rows += 1 + #widget style + wrappers = dict( + table=(TABLE,TR,TD), + ul=(DIV,UL,LI), + divs=(CAT,DIV,DIV) + ) + parent, child, inner = wrappers[attributes.get('style','table')] + for r_index in range(rows): tds = [] for k, v in options[r_index*cols:(r_index+1)*cols]: if k in values: r_value = k else: r_value = [] - tds.append(TD(INPUT(_type='checkbox', _name=field.name, - requires=attr.get('requires', None), - hideerror=True, _value=k, - value=r_value), v)) - opts.append(TR(tds)) + tds.append(inner(INPUT(_type='checkbox', + _id='%s%s' % (field.name,k), + _name=field.name, + requires=attr.get('requires', None), + hideerror=True, _value=k, + value=r_value), + LABEL(v,_for='%s%s' % (field.name,k)))) + opts.append(child(tds)) if opts: opts[-1][0][0]['hideerror'] = False - return TABLE(*opts, **attr) + return parent(*opts, **attr) class PasswordWidget(FormWidget): DEFAULT_PASSWORD_DISPLAY = 8*('*') @@ -434,11 +458,14 @@ attr = UploadWidget._attributes(field, default, **attributes) inp = INPUT(**attr) if download_url and value: - url = download_url + '/' + value + if callable(download_url): + url = download_url(value) + else: + url = download_url + '/' + value (br, image) = ('', '') if UploadWidget.is_image(value): br = BR() image = IMG(_src = url, _width = UploadWidget.DEFAULT_WIDTH) @@ -446,13 +473,15 @@ if requires == [] or isinstance(requires, IS_EMPTY_OR): inp = DIV(inp, '[', A(UploadWidget.GENERIC_DESCRIPTION, _href = url), '|', INPUT(_type='checkbox', - _name=field.name + UploadWidget.ID_DELETE_SUFFIX), - UploadWidget.DELETE_FILE, - ']', br, image) + _name=field.name + UploadWidget.ID_DELETE_SUFFIX, + _id=field.name + UploadWidget.ID_DELETE_SUFFIX), + LABEL(UploadWidget.DELETE_FILE, + _for=field.name + UploadWidget.ID_DELETE_SUFFIX), + ']', br, image) else: inp = DIV(inp, '[', A(UploadWidget.GENERIC_DESCRIPTION, _href = url), ']', br, image) return inp @@ -472,11 +501,14 @@ """ inp = UploadWidget.GENERIC_DESCRIPTION if download_url and value: - url = download_url + '/' + value + if callable(download_url): + url = download_url(value) + else: + url = download_url + '/' + value if UploadWidget.is_image(value): inp = IMG(_src = url, _width = UploadWidget.DEFAULT_WIDTH) inp = A(inp, _href = url) return inp @@ -515,11 +547,11 @@ self.is_reference = True self.fields.append(id_field) else: self.is_reference = False if hasattr(request,'application'): - self.url = Url(r=request, args=request.args) + self.url = URL(args=request.args) self.callback() else: self.url = request def callback(self): if self.keyword in self.request.vars: @@ -544,11 +576,11 @@ raise HTTP(200,'') def __call__(self,field,value,**attributes): default = dict( _type = 'text', - value = (value!=None and str(value)) or '', + value = (not value is None and str(value)) or '', ) attr = StringWidget._attributes(field, default, **attributes) div_id = self.keyword+'_div' attr['_autocomplete']='off' if self.is_reference: @@ -685,11 +717,11 @@ """ SQLFORM(db.table, record=None, fields=['name'], labels={'name': 'Your name'}, - linkto=URL(r=request, f='table/db/') + linkto=URL(f='table/db/') """ self.ignore_rw = ignore_rw self.formstyle = formstyle nbsp = XML(' ') # Firefox2 does not display fields with blanks @@ -697,11 +729,11 @@ ofields = fields keyed = hasattr(table,'_primarykey') # if no fields are provided, build it from the provided table # will only use writable or readable fields, unless forced to ignore - if fields == None: + if fields is None: fields = [f.name for f in table if (ignore_rw or f.writable or f.readable) and not f.compute] self.fields = fields # make sure we have an id if self.fields[0] != table.fields[0] and \ @@ -736,33 +768,33 @@ self.custom.comment = Storage() self.custom.widget = Storage() self.custom.linkto = Storage() sep = separator or '' - + for fieldname in self.fields: if fieldname.find('.') >= 0: continue field = self.table[fieldname] comment = None if comments: comment = col3.get(fieldname, field.comment) - if comment == None: + if comment is None: comment = '' self.custom.comment[fieldname] = comment - if labels != None and fieldname in labels: + if not labels is None and fieldname in labels: label = labels[fieldname] else: label = field.label self.custom.label[fieldname] = label field_id = '%s_%s' % (table._tablename, fieldname) - label = LABEL(label, sep, _for=field_id, + label = LABEL(label, label and sep, _for=field_id, _id=field_id+SQLFORM.ID_LABEL_SUFFIX) row_id = field_id+SQLFORM.ID_ROW_SUFFIX if field.type == 'id': self.custom.dspval.id = nbsp @@ -1082,27 +1114,28 @@ value = self.vars[fieldname] elif self.record: value = self.record[fieldname] else: value = self.table[fieldname].default + if field.type.startswith('list:') and \ + isinstance(value, str): + value = [value] row_id = '%s_%s%s' % (self.table, fieldname, SQLFORM.ID_ROW_SUFFIX) widget = field.widget(field, value) self.field_parent[row_id].components = [ widget ] if not field.type.startswith('list:'): self.field_parent[row_id]._traverse(False, hideerror) self.custom.widget[ fieldname ] = widget + self.accepted = ret return ret if record_id and str(record_id) != str(self.record_id): raise SyntaxError, 'user is tampering with form\'s record_id: ' \ '%s != %s' % (record_id, self.record_id) - if record_id and dbio: - if keyed: - self.vars.update(record_id) - else: - self.vars.id = self.record.id + if record_id and dbio and not keyed: + self.vars.id = self.record.id if requested_delete and self.custom.deletable: if dbio: if keyed: qry = reduce(lambda x, y: x & y, @@ -1111,10 +1144,11 @@ qry = self.table._id == self.record.id self.table._db(qry).delete() self.errors.clear() for component in self.elements('input, select, textarea'): component['_disabled'] = True + self.accepted = True return True for fieldname in self.fields: if not fieldname in self.table.fields: continue @@ -1121,11 +1155,11 @@ if not self.ignore_rw and not self.table[fieldname].writable: ### this happens because FORM has no knowledge of writable ### and thinks that a missing boolean field is a None if self.table[fieldname].type == 'boolean' and \ - self.vars.get(fieldname, True) == None: + self.vars.get(fieldname, True) is None: del self.vars[fieldname] continue field = self.table[fieldname] if field.type == 'id': @@ -1140,11 +1174,11 @@ PasswordWidget.DEFAULT_PASSWORD_DISPLAY: continue # do not update if password was not changed elif field.type == 'upload': f = self.vars[fieldname] fd = '%s__delete' % fieldname - if f == '' or f == None: + if f == '' or f is None: if self.vars.get(fd, False) or not self.record: fields[fieldname] = '' else: fields[fieldname] = self.record[fieldname] self.vars[fieldname] = fields[fieldname] @@ -1164,45 +1198,47 @@ # proposed by Hamdy (accept?) do we need fields at this point? self.vars[fieldname] = fields[fieldname] continue elif fieldname in self.vars: fields[fieldname] = self.vars[fieldname] - elif field.default == None and field.type != 'blob': + elif field.default is None and field.type != 'blob': self.errors[fieldname] = 'no data' + self.accepted = False return False value = fields.get(fieldname,None) if field.type == 'list:string': if not isinstance(value, (tuple, list)): fields[fieldname] = value and [value] or [] elif isinstance(field.type,str) and field.type.startswith('list:'): if not isinstance(value, list): fields[fieldname] = [safe_int(x) for x in (value and [value] or [])] elif field.type == 'integer': - if value != None: + if not value is None: fields[fieldname] = safe_int(value) elif field.type.startswith('reference'): - if value != None and isinstance(self.table, Table) and not keyed: + if not value is None and isinstance(self.table, Table) and not keyed: fields[fieldname] = safe_int(value) elif field.type == 'double': - if value != None: + if not value is None: fields[fieldname] = safe_float(value) for fieldname in self.vars: if fieldname != 'id' and fieldname in self.table.fields\ and not fieldname in fields and not fieldname\ in request_vars: fields[fieldname] = self.vars[fieldname] - if dbio: + if dbio: if 'delete_this_record' in fields: # this should never happen but seems to happen to some del fields['delete_this_record'] for field in self.table: - if not field.name in fields and field.writable==False: + if not field.name in fields and field.writable==False \ + and field.update is None: if record_id: fields[field.name] = self.record[field.name] - elif self.table[field.name].default!=None: + elif not self.table[field.name].default is None: fields[field.name] = self.table[field.name].default if keyed: if reduce(lambda x, y: x and y, record_id.values()): # if record_id if fields: qry = reduce(lambda x, y: x & y, @@ -1219,10 +1255,11 @@ self.vars.id = self.record.id if fields: self.table._db(self.table._id == self.record.id).update(**fields) else: self.vars.id = self.table.insert(**fields) + self.accepted = ret return ret @staticmethod def factory(*fields, **attributes): """ @@ -1239,11 +1276,556 @@ # So it won't interfear with SQLDB.define_table if 'table_name' in attributes: del attributes['table_name'] - return SQLFORM(DAL(None).define_table(table_name, *fields), **attributes) + return SQLFORM(DAL(None).define_table(table_name, *fields), + **attributes) + + @staticmethod + def grid(query, + fields=None, + field_id=None, + left=None, + headers={}, + columns=None, + orderby=None, + searchable=True, + sortable=True, + paginate=20, + deletable=True, + editable=True, + details=True, + selectable=None, + create=True, + csv=True, + links=None, + upload = '', + args=[], + user_signature = True, + maxtextlengths={}, + maxtextlength=20, + onvalidation=None, + oncreate=None, + onupdate=None, + ondelete=None, + sorter_icons=('[^]','[v]'), + ui = 'web2py', + showbuttontext=True, + _class="web2py_grid", + formname='web2py_grid', + ): + + # jQuery UI ThemeRoller classes (empty if ui is disabled) + if ui == 'jquery-ui': + ui = dict(widget='ui-widget', + header='ui-widget-header', + content='ui-widget-content', + default='ui-state-default', + cornerall='ui-corner-all', + cornertop='ui-corner-top', + cornerbottom='ui-corner-bottom', + button='ui-button-text-icon-primary', + buttontext='ui-button-text', + buttonadd='ui-icon ui-icon-plusthick', + buttonback='ui-icon ui-icon-arrowreturnthick-1-w', + buttonexport='ui-icon ui-icon-transferthick-e-w', + buttondelete='ui-icon ui-icon-trash', + buttonedit='ui-icon ui-icon-pencil', + buttontable='ui-icon ui-icon-triangle-1-e', + buttonview='ui-icon ui-icon-zoomin', + ) + elif ui == 'web2py': + ui = dict(widget='', + header='', + content='', + default='', + cornerall='', + cornertop='', + cornerbottom='', + button='button', + buttontext='buttontext button', + buttonadd='icon plus', + buttonback='icon leftarrow', + buttonexport='icon downarrow', + buttondelete='icon trash', + buttonedit='icon pen', + buttontable='icon rightarrow', + buttonview='icon magnifier', + ) + elif not isinstance(ui,dict): + raise RuntimeError,'SQLFORM.grid ui argument must be a dictionary' + + from gluon import current, redirect + db = query._db + T = current.T + request = current.request + session = current.session + response = current.response + wenabled = (not user_signature or (session.auth and session.auth.user)) + #create = wenabled and create + #editable = wenabled and editable + deletable = wenabled and deletable + def url(**b): + b['args'] = args+b.get('args',[]) + b['user_signature'] = user_signature + return URL(**b) + + def gridbutton(buttonclass='buttonadd',buttontext='Add',buttonurl=url(args=[]),callback=None,delete=None): + if showbuttontext: + if callback: + return A(SPAN(_class=ui.get(buttonclass,'')), + SPAN(T(buttontext),_title=buttontext, + _class=ui.get('buttontext','')), + callback=callback,delete=delete, + _class=ui.get('button','')) + else: + return A(SPAN(_class=ui.get(buttonclass,'')), + SPAN(T(buttontext),_title=buttontext, + _class=ui.get('buttontext','')), + _href=buttonurl,_class=ui.get('button','')) + else: + if callback: + return A(SPAN(_class=ui.get(buttonclass,'')), + callback=callback,delete=delete, + _title=buttontext,_class=ui.get('buttontext','')) + else: + return A(SPAN(_class=ui.get(buttonclass,'')), + _href=buttonurl,_title=buttontext, + _class=ui.get('buttontext','')) + + dbset = db(query) + tables = [db[tablename] for tablename in db._adapter.tables( + dbset.query)] + if not fields: + fields = reduce(lambda a,b:a+b, + [[field for field in table] for table in tables]) + if not field_id: + field_id = tables[0]._id + table = field_id.table + tablename = table._tablename + referrer = session.get('_web2py_grid_referrer_'+formname, url()) + def check_authorization(): + if user_signature: + if not URL.verify(request,user_signature=user_signature): + session.flash = T('not authorized') + redirect(referrer) + if upload=='': + upload = lambda filename: url(args=['download',filename]) + if len(request.args)>1 and request.args[-2]=='download': + check_authorization() + stream = response.download(request,db) + raise HTTP(200,stream,**response.headers) + + def buttons(edit=False,view=False,record=None): + buttons = DIV(gridbutton('buttonback', 'Back', referrer), + _class='form_header row_buttons %(header)s %(cornertop)s' % ui) + if edit: + args = ['edit',table._tablename,request.args[-1]] + buttons.append(gridbutton('buttonedit', 'Edit', + url(args=args))) + if view: + args = ['view',table._tablename,request.args[-1]] + buttons.append(gridbutton('buttonview', 'View', + url(args=args))) + if record and links: + for link in links: + buttons.append(link(record)) + return buttons + + formfooter = DIV( + _class='form_footer row_buttons %(header)s %(cornerbottom)s' % ui) + + create_form = edit_form = None + + if create and len(request.args)>1 and request.args[-2]=='new': + check_authorization() + table = db[request.args[-1]] + create_form = SQLFORM( + table, + _class='web2py_form' + ).process(next=referrer, + onvalidation=onvalidation, + onsuccess=oncreate, + formname=formname) + res = DIV(buttons(),create_form,formfooter,_class=_class) + res.create_form = create_form + res.edit_form = None + res.update_form = None + return res + elif details and len(request.args)>2 and request.args[-3]=='view': + check_authorization() + table = db[request.args[-2]] + record = table(request.args[-1]) or redirect(URL('error')) + form = SQLFORM(table,record,upload=upload, + readonly=True,_class='web2py_form') + res = DIV(buttons(edit=editable,record=record),form, + formfooter,_class=_class) + res.create_form = None + res.edit_form = None + res.update_form = None + return res + elif editable and len(request.args)>2 and request.args[-3]=='edit': + check_authorization() + table = db[request.args[-2]] + record = table(request.args[-1]) or redirect(URL('error')) + edit_form = SQLFORM(table,record,upload=upload, + deletable=deletable, + _class='web2py_form') + edit_form.process(formname=formname, + onvalidation=onvalidation, + onsuccess=onupdate, + next=referrer) + res = DIV(buttons(view=details,record=record), + edit_form,formfooter,_class=_class) + res.create_form = None + res.edit_form = edit_form + res.update_form = None + return res + elif deletable and len(request.args)>2 and request.args[-3]=='delete': + check_authorization() + table = db[request.args[-2]] + ret = db(table.id==request.args[-1]).delete() + if ondelete: + return ondelete(table,request.args[-2],ret) + return ret + elif csv and len(request.args)>0 and request.args[-1]=='csv': + check_authorization() + response.headers['Content-Type'] = 'text/csv' + response.headers['Content-Disposition'] = \ + 'attachment;filename=rows.csv;' + raise HTTP(200,str(dbset.select()), + **{'Content-Type':'text/csv', + 'Content-Disposition':'attachment;filename=rows.csv;'}) + elif request.vars.records and not isinstance( + request.vars.records,list): + request.vars.records=[request.vars.records] + elif not request.vars.records: + request.vars.records=[] + def OR(a,b): return a|b + def AND(a,b): return a&b + + session['_web2py_grid_referrer_'+formname] = \ + URL(args=request.args,vars=request.vars, + user_signature=user_signature) + console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui) + error = None + search_form = None + if searchable: + form = FORM(INPUT(_name='keywords',_value=request.vars.keywords, + _id='web2py_keywords'), + INPUT(_type='submit',_value=T('Search')), + INPUT(_type='submit',_value=T('Clear'), + _onclick="jQuery('#web2py_keywords').val('');"), + _method="GET",_action=url()) + search_form = form + console.append(form) + key = request.vars.get('keywords','').strip() + if searchable==True: + subquery = None + if key and not ' ' in key: + SEARCHABLE_TYPES = ('string','text','list:string') + parts = [field.contains(key) for field in fields \ + if field.type in SEARCHABLE_TYPES] + else: + parts = None + if parts: + subquery = reduce(OR,parts) + else: + try: + subquery = smart_query(fields,key) + except RuntimeError: + subquery = None + error = T('Invalid query') + else: + subquery = searchable(key,fields) + if subquery: + dbset = dbset(subquery) + try: + if left: + nrows = dbset.select('count(*)',left=left).first()['count(*)'] + else: + nrows = dbset.count() + except: + nrows = 0 + error = T('Unsupported query') + + search_actions = DIV(_class='web2py_search_actions') + if create: + search_actions.append(gridbutton( + buttonclass='buttonadd', + buttontext='Add', + buttonurl=url(args=['new',tablename]))) + if csv: + search_actions.append(gridbutton( + buttonclass='buttonexport', + buttontext='Export', + buttonurl=url(args=['csv']))) + + console.append(search_actions) + + message = error or T('%(nrows)s records found' % dict(nrows=nrows)) + + console.append(DIV(message,_class='web2py_counter')) + + order = request.vars.order or '' + if sortable: + if order and not order=='None': + if order[:1]=='~': + sign, rorder = '~', order[1:] + else: + sign, rorder = '', order + tablename,fieldname = rorder.split('.',1) + if sign=='~': + orderby=~db[tablename][fieldname] + else: + orderby=db[tablename][fieldname] + + head = TR(_class=ui.get('header','')) + if selectable: + head.append(TH(_class=ui.get('default',''))) + for field in fields: + if columns and not str(field) in columns: continue + if not field.readable: continue + key = str(field) + header = headers.get(str(field), + hasattr(field,'label') and field.label or key) + if sortable: + if key == order: + key, marker = '~'+order, sorter_icons[0] + elif key == order[1:]: + marker = sorter_icons[1] + else: + marker = '' + header = A(header,marker,_href=url(vars=dict( + keywords=request.vars.keywords or '', + order=key))) + head.append(TH(header, _class=ui.get('default',''))) + + for link in links or []: + if isinstance(link,dict): + head.append(TH(link['header'], _class=ui.get('default',''))) + + head.append(TH(_class=ui.get('default',''))) + + paginator = UL() + if paginate and paginate0: + paginator.append(LI(self_link('<<',0))) + if page>1: + paginator.append(LI(self_link('<',page-1))) + pages = range(max(0,page-5),min(page+5,npages-1)) + for p in pages: + if p == page: + paginator.append(LI(A(p+1,_onclick='return false'), + _class='current')) + else: + paginator.append(LI(self_link(p+1,p))) + if page',page+1))) + if page>',npages-1))) + else: + limitby = None + + rows = dbset.select(left=left,orderby=orderby,limitby=limitby,*fields) + if not searchable and not rows: return DIV(T('No records found')) + if rows: + htmltable = TABLE(THEAD(head)) + tbody = TBODY() + numrec=0 + for row in rows: + if numrec % 2 == 0: + classtr = 'even' + else: + classtr = 'odd' + numrec+=1 + id = row[field_id] + if len(tables)>1 or row.get('_extra',None): + rrow = row[field._tablename] + else: + rrow = row + tr = TR(_class=classtr) + if selectable: + tr.append(INPUT(_type="checkbox",_name="records",_value=id, + value=request.vars.records)) + for field in fields: + if columns and not str(field) in columns: continue + if not field.readable: continue + if field.type=='blob': continue + value = row[field] + if field.represent: + try: + value=field.represent(value,rrow) + except KeyError: + pass + elif field.type=='boolean': + value = INPUT(_type="checkbox",_checked = value, + _disabled=True) + elif field.type=='upload': + if value: + if callable(upload): + value = A('File', _href=upload(value)) + elif upload: + value = A('File', + _href='%s/%s' % (upload, value)) + else: + value = '' + elif isinstance(value,str) and len(value)>maxtextlength: + value=value[:maxtextlengths.get(str(field),maxtextlength)]+'...' + else: + value=field.formatter(value) + tr.append(TD(value)) + row_buttons = TD(_class='row_buttons') + for link in links or []: + if isinstance(link, dict): + tr.append(TD(link['body'](row))) + else: + row_buttons.append(link(row)) + if details and (not callable(details) or details(row)): + row_buttons.append(gridbutton( + 'buttonview', 'View', + url(args=['view',tablename,id]))) + if editable and (not callable(editable) or editable(row)): + row_buttons.append(gridbutton( + 'buttonedit', 'Edit', + url(args=['edit',tablename,id]))) + if deletable and (not callable(deletable) or deletable(row)): + row_buttons.append(gridbutton( + 'buttondelete', 'Delete', + callback=url(args=['delete',tablename,id]), + delete='tr')) + tr.append(row_buttons) + tbody.append(tr) + htmltable.append(tbody) + if selectable: + htmltable = FORM(htmltable,INPUT(_type="submit")) + if htmltable.process(formname=formname).accepted: + records = [int(r) for r in htmltable.vars.records or []] + selectable(records) + redirect(referrer) + else: + htmltable = DIV(T('No records found')) + res = DIV(console, + DIV(htmltable,_class="web2py_table"), + DIV(paginator,_class=\ + "web2py_paginator %(header)s %(cornerbottom)s" % ui), + _class='%s %s' % (_class, ui.get('widget',''))) + res.create_form = create_form + res.edit_form = edit_form + res.search_form = search_form + return res + + @staticmethod + def smartgrid(table, constraints=None, links=None, + linked_tables=None, user_signature=True, + **kwargs): + """ + @auth.requires_login() + def index(): + db.define_table('person',Field('name'),format='%(name)s') + db.define_table('dog', + Field('name'),Field('owner',db.person),format='%(name)s') + db.define_table('comment',Field('body'),Field('dog',db.dog)) + if db(db.person).isempty(): + from gluon.contrib.populate import populate + populate(db.person,300) + populate(db.dog,300) + populate(db.comment,1000) + db.commit() + form=SQLFORM.smartgrid(db[request.args(0) or 'person']) #*** + return dict(form=form) + + *** builds a complete interface to navigate all tables links + to the request.args(0) + table: pagination, search, view, edit, delete, + children, parent, etc. + + constraints is a dict {'table',query} that limits which + records can be accessible + links is a list of lambda row: A(....) that will add buttons + linked_tables is a optional list of tablenames of tables to be linked + + """ + from gluon import current, A, URL, DIV, H3, redirect + request, T = current.request, current.T + db = table._db + if links is None: links = [] + if constraints is None: constraints = {} + breadcrumbs = [] + if request.args(0) != table._tablename: + request.args=[table._tablename] + try: + args = 1 + previous_tablename,previous_fieldname,previous_id = \ + table._tablename,None,None + while len(request.args)>args: + key = request.args(args) + if '.' in key: + id = request.args(args+1) + tablename,fieldname = key.split('.',1) + table = db[tablename] + field = table[fieldname] + field.default = id + referee = field.type[10:] + if referee!=previous_tablename: + raise HTTP(400) + cond = constraints.get(referee,None) + if cond: + record = db(db[referee].id==id)(cond).select().first() + else: + record = db[referee](id) + if previous_id: + if record[previous_fieldname] != int(previous_id): + raise HTTP(400) + previous_tablename,previous_fieldname,previous_id = \ + tablename,fieldname,id + try: + name = db[referee]._format % record + except TypeError: + name = id + breadcrumbs += [A(T(referee), + _href=URL(args=request.args[:args])),' ', + A(name, + _href=URL(args=request.args[:args]+[ + 'view',referee,id],user_signature=True)), + ' > '] + args+=2 + else: + break + if args>1: + query = (field == id) + if linked_tables is None or referee in linked_tables: + field.represent = lambda id,r=None,referee=referee,rep=field.represent: A(rep(id),_href=URL(args=request.args[:args]+['view',referee,id], user_signature=user_signature)) + except (KeyError,ValueError,TypeError): + redirect(URL(args=table._tablename)) + if args==1: + query = table.id>0 + if table._tablename in constraints: + query = query&constraints[table._tablename] + for tablename,fieldname in table._referenced_by: + if linked_tables is None or tablename in linked_tables: + args0 = tablename+'.'+fieldname + links.append(lambda row,t=T(tablename),args=args,args0=args0:\ + A(SPAN(t),_href=URL(args=request.args[:args]+[args0,row.id]))) + grid=SQLFORM.grid(query,args=request.args[:args],links=links, + user_signature=user_signature,**kwargs) + if isinstance(grid,DIV): + breadcrumbs.append(A(T(table._tablename), + _href=URL(args=request.args[:args]))) + grid.insert(0,DIV(H3(*breadcrumbs),_class='web2py_breadcrumbs')) + return grid class SQLTABLE(TABLE): """ @@ -1277,46 +1859,46 @@ More advanced linkto example:: def mylink(field, type, ref): - return URL(r=request, args=[field]) + return URL(args=[field]) rows = db.select(db.sometable.ALL) table = SQLTABLE(rows, linkto=mylink) This will link rows[id] to current_app/current_controlle/current_function/value_of_id - + New Implements: 24 June 2011: ----------------------------- - + :param selectid: The id you want to select :param renderstyle: Boolean render the style with the table - + :param extracolums = [{'label':A('Extra',_href='#'), 'class': '', #class name of the header 'width':'', #width in pixels or % - 'content':lambda row, rc: A('Edit',_href='edit/%s'%row.id), + 'content':lambda row, rc: A('Edit',_href='edit/%s'%row.id), 'selected': False #agregate class selected to this column }] - - + + :param headers = {'table.id':{'label':'Id', 'class':'', #class name of the header 'width':'', #width in pixels or % 'truncate': 16, #truncate the content to... 'selected': False #agregate class selected to this column - }, + }, 'table.myfield':{'label':'My field', 'class':'', #class name of the header 'width':'', #width in pixels or % 'truncate': 16, #truncate the content to... 'selected': False #agregate class selected to this column }, } - + table = SQLTABLE(rows, headers=headers, extracolums=extracolums) """ @@ -1326,19 +1908,20 @@ linkto=None, upload=None, orderby=None, headers={}, truncate=16, - columns=None, + columns=None, th_link='', extracolumns=None, selectid=None, renderstyle=False, **attributes ): TABLE.__init__(self, **attributes) + self.components = [] self.attributes = attributes self.sqlrows = sqlrows (components, row) = (self.components, []) if not sqlrows: @@ -1353,11 +1936,11 @@ headers = {} for c in columns: (t,f) = c.split('.') field = sqlrows.db[t][f] headers[c] = field.label - if headers!=None: + if not headers is None: for c in columns:#new implement dict if isinstance(headers.get(c, c), dict): coldict = headers.get(c, c) attrcol = dict() if coldict['width']!="": @@ -1368,35 +1951,35 @@ elif orderby: row.append(TH(A(headers.get(c, c), _href=th_link+'?orderby=' + c))) else: row.append(TH(headers.get(c, c))) - + if extracolumns:#new implement dict for c in extracolumns: attrcol = dict() if c['width']!="": attrcol.update(_width=c['width']) if c['class']!="": attrcol.update(_class=c['class']) row.append(TH(c['label'],**attrcol)) - + components.append(THEAD(TR(*row))) - + tbody = [] for (rc, record) in enumerate(sqlrows): row = [] if rc % 2 == 0: _class = 'even' else: _class = 'odd' - - if selectid!=None:#new implement + + if not selectid is None: #new implement if record.id==selectid: _class += ' rowselected' - + for colname in columns: if not table_field.match(colname): if "_extra" in record and colname in record._extra: r = record._extra[colname] row.append(TD(r)) @@ -1461,31 +2044,31 @@ r = 'file' else: r = '' elif field.type in ['string','text']: r = str(field.formatter(r)) - ur = unicode(r, 'utf8') + ur = unicode(r, 'utf8') if headers!={}: #new implement dict if isinstance(headers[colname],dict): if isinstance(headers[colname]['truncate'], int) \ and len(ur)>headers[colname]['truncate']: r = ur[:headers[colname]['truncate'] - 3] r = r.encode('utf8') + '...' - elif truncate!=None and len(ur) > truncate: + elif not truncate is None and len(ur) > truncate: r = ur[:truncate - 3].encode('utf8') + '...' - + attrcol = dict()#new implement dict if headers!={}: if isinstance(headers[colname],dict): colclass=headers[colname]['class'] if headers[colname]['selected']: colclass= str(headers[colname]['class'] + " colselected").strip() if colclass!="": attrcol.update(_class=colclass) - + row.append(TD(r,**attrcol)) - + if extracolumns:#new implement dict for c in extracolumns: attrcol = dict() colclass=c['class'] if c['selected']: @@ -1492,21 +2075,21 @@ colclass= str(c['class'] + " colselected").strip() if colclass!="": attrcol.update(_class=colclass) contentfunc = c['content'] row.append(TD(contentfunc(record, rc),**attrcol)) - + tbody.append(TR(_class=_class, *row)) - + if renderstyle: components.append(STYLE(self.style())) - + components.append(TBODY(*tbody)) - - + + def style(self): - + css = ''' table tbody tr.odd { background-color: #DFD; } table tbody tr.even { @@ -1520,11 +2103,13 @@ } table tbody tr:hover { background: #DDF; } ''' - + return css form_factory = SQLFORM.factory # for backward compatibility, deprecated + + ADDED gluon/sqlhtml.pyc Index: gluon/sqlhtml.pyc ================================================================== --- gluon/sqlhtml.pyc +++ gluon/sqlhtml.pyc cannot compute difference between binary files Index: gluon/storage.py ================================================================== --- gluon/storage.py +++ gluon/storage.py @@ -59,11 +59,11 @@ return self[key] else: return None def __setattr__(self, key, value): - if value == None: + if value is None: if key in self: del self[key] else: self[key] = value @@ -219,6 +219,8 @@ return value if __name__ == '__main__': import doctest doctest.testmod() + + ADDED gluon/storage.pyc Index: gluon/storage.pyc ================================================================== --- gluon/storage.pyc +++ gluon/storage.pyc cannot compute difference between binary files Index: gluon/streamer.py ================================================================== --- gluon/streamer.py +++ gluon/streamer.py @@ -22,12 +22,12 @@ DEFAULT_CHUNK_SIZE = 64*1024 def streamer(stream, chunk_size = DEFAULT_CHUNK_SIZE, bytes = None): offset = 0 - while bytes == None or offset < bytes: - if bytes != None and bytes - offset < chunk_size: + while bytes is None or offset < bytes: + if not bytes is None and bytes - offset < chunk_size: chunk_size = bytes - offset data = stream.read(chunk_size) length = len(data) if not length: break @@ -104,6 +104,8 @@ if request and request.env.web2py_use_wsgi_file_wrapper: wrapped = request.env.wsgi_file_wrapper(stream, chunk_size) else: wrapped = streamer(stream, chunk_size=chunk_size, bytes=bytes) raise HTTP(status, wrapped, **headers) + + ADDED gluon/streamer.pyc Index: gluon/streamer.pyc ================================================================== --- gluon/streamer.pyc +++ gluon/streamer.pyc cannot compute difference between binary files Index: gluon/template.py ================================================================== --- gluon/template.py +++ gluon/template.py @@ -926,6 +926,8 @@ if __name__ == '__main__': import doctest doctest.testmod() + + ADDED gluon/template.pyc Index: gluon/template.pyc ================================================================== --- gluon/template.pyc +++ gluon/template.pyc cannot compute difference between binary files Index: gluon/tests/test_router.py ================================================================== --- gluon/tests/test_router.py +++ gluon/tests/test_router.py @@ -338,11 +338,11 @@ self.assertEqual(filter_url('http://domain.com/', app=True), 'init') self.assertEqual(filter_url('http://domain.com/abc', app=True), 'init') self.assertEqual(filter_url('http://domain1.com/abc', app=True), 'app1') self.assertEqual(filter_url('http://www.domain1.com/abc', app=True), 'app1') self.assertEqual(filter_url('http://domain2.com/abc', app=True), 'app2') - self.assertEqual(filter_url('http://domain2.com/admin', app=True), 'admin') + self.assertEqual(filter_url('http://domain2.com/admin', app=True), 'app2') self.assertEqual(filter_url('http://domain.com/goodapp', app=True), 'goodapp') self.assertRaises(HTTP, filter_url, 'http://domain.com/bad!app', app=True) try: # 2.7+ only @@ -373,20 +373,23 @@ "domain2.com" : "app2a", "domain2.com:8080" : "app2b", # two domains, same app, two controllers "domain3a.com" : "app3/c3a", "domain3b.com" : "app3/c3b", + # two domains, same app & controller, two functions + "domain4a.com" : "app4/c4/f4a", + "domain4b.com" : "app4/c4/f4b", # http vs https "domain6.com:80" : "app6", "domain6.com:443" : "app6s", }, ), app1 = dict( default_controller = 'c1', default_function = 'f1', controllers = ['c1'], exclusive_domain=True, ), app2a = dict( default_controller = 'c2a', default_function = 'f2a', controllers = ['c2a'], ), app2b = dict( default_controller = 'c2b', default_function = 'f2b', controllers = ['c2b'], ), app3 = dict( controllers = ['c3a', 'c3b'], ), - app4 = dict( default_controller = 'c4', controllers = ['c4'], domain = 'domain4.com' ), + app4 = dict( default_controller = 'c4', controllers = ['c4']), app5 = dict( default_controller = 'c5', controllers = ['c5'], domain = 'localhost' ), app6 = dict( default_controller = 'c6', default_function = 'f6', controllers = ['c6'], ), app6s = dict( default_controller = 'c6s', default_function = 'f6s', controllers = ['c6s'], ), ) @@ -394,10 +397,11 @@ self.assertEqual(filter_url('http://domain1.com/abc'), '/app1/c1/abc') self.assertEqual(filter_url('http://domain1.com/c1/abc'), '/app1/c1/abc') self.assertEqual(filter_url('http://domain1.com/abc.html'), '/app1/c1/abc') self.assertEqual(filter_url('http://domain1.com/abc.css'), '/app1/c1/abc.css') self.assertEqual(filter_url('http://domain1.com/index/abc'), "/app1/c1/index ['abc']") + self.assertEqual(filter_url('http://domain2.com/app1'), "/app2a/c2a/app1") self.assertEqual(filter_url('https://domain1.com/app1/ctr/fcn', domain=('app1',None), out=True), "/ctr/fcn") self.assertEqual(filter_url('https://www.domain1.com/app1/ctr/fcn', domain=('app1',None), out=True), "/ctr/fcn") self.assertEqual(filter_url('http://domain2.com/abc'), '/app2a/c2a/abc') @@ -418,12 +422,15 @@ self.assertEqual(filter_url('http://domain3a.com/app3/c3a/fcn', domain=('app3','c3a'), out=True), "/fcn") self.assertEqual(filter_url('http://domain3a.com/app3/c3a/fcn', domain=('app3','c3b'), out=True), "/c3a/fcn") self.assertEqual(filter_url('http://domain3a.com/app3/c3a/fcn', domain=('app1',None), out=True), "/app3/c3a/fcn") - self.assertEqual(filter_url('http://domain4.com/abc'), '/app4/c4/abc') - self.assertEqual(filter_url('https://domain4.com/app4/c4/fcn', domain=('app4',None), out=True), "/fcn") + self.assertEqual(filter_url('http://domain4a.com/abc'), '/app4/c4/abc') + self.assertEqual(filter_url('https://domain4a.com/app4/c4/fcn', domain=('app4',None), out=True), "/fcn") + + self.assertEqual(filter_url('http://domain4a.com'), '/app4/c4/f4a') + self.assertEqual(filter_url('http://domain4b.com'), '/app4/c4/f4b') self.assertEqual(filter_url('http://localhost/abc'), '/app5/c5/abc') self.assertEqual(filter_url('http:///abc'), '/app5/c5/abc') # test null host => localhost self.assertEqual(filter_url('https://localhost/app5/c5/fcn', domain=('app5',None), out=True), "/fcn") @@ -523,41 +530,53 @@ init = dict( controllers = ['default'], ), app = dict( controllers = ['default', 'ctr'], - functions = ['index', 'user', 'help'], + functions = dict( + default=['index', 'user', 'help'], + ctr=['ctrf1', 'ctrf2', 'ctrf3'], + ), + default_function = dict( + default='index', + ctr='ctrf1', + ), ), app2 = dict( controllers = ['default', 'ctr'], functions = ['index', 'user', 'help'], ), ) load(rdict=router_functions) + + # outbound self.assertEqual(str(URL(a='init', c='default', f='f', args=['arg1'])), "/init/f/arg1") self.assertEqual(str(URL(a='init', c='default', f='index', args=['arg1'])), "/init/index/arg1") + self.assertEqual(str(URL(a='app', c='default', f='index', args=['arg1'])), "/arg1") self.assertEqual(str(URL(a='app', c='default', f='user', args=['arg1'])), "/user/arg1") self.assertEqual(str(URL(a='app', c='default', f='user', args=['index'])), "/user/index") self.assertEqual(str(URL(a='app', c='default', f='index', args=['index'])), "/index/index") self.assertEqual(str(URL(a='app', c='default', f='index', args=['init'])), "/index/init") self.assertEqual(str(URL(a='app', c='default', f='index', args=['ctr'])), "/index/ctr") self.assertEqual(str(URL(a='app', c='ctr', f='index', args=['arg'])), "/ctr/index/arg") + self.assertEqual(str(URL(a='app', c='ctr', f='ctrf1', args=['arg'])), "/ctr/arg") + self.assertEqual(str(URL(a='app', c='ctr', f='ctrf1', args=['ctrf2'])), "/ctr/ctrf1/ctrf2") self.assertEqual(str(URL(a='app2', c='default', f='index', args=['arg1'])), "/app2/arg1") self.assertEqual(str(URL(a='app2', c='default', f='user', args=['arg1'])), "/app2/user/arg1") self.assertEqual(str(URL(a='app2', c='default', f='user', args=['index'])), "/app2/user/index") self.assertEqual(str(URL(a='app2', c='default', f='index', args=['index'])), "/app2/index/index") self.assertEqual(str(URL(a='app2', c='default', f='index', args=['init'])), "/app2/index/init") self.assertEqual(str(URL(a='app2', c='default', f='index', args=['ctr'])), "/app2/index/ctr") + # inbound self.assertEqual(filter_url('http://d.com/arg'), "/app/default/index ['arg']") self.assertEqual(filter_url('http://d.com/user'), "/app/default/user") self.assertEqual(filter_url('http://d.com/user/arg'), "/app/default/user ['arg']") - self.assertEqual(filter_url('http://d.com/ctr'), "/app/ctr/index") - self.assertEqual(filter_url('http://d.com/ctr/index/arg'), "/app/ctr/index ['arg']") - self.assertEqual(filter_url('http://d.com/ctr/arg'), "/app/ctr/arg") + self.assertEqual(filter_url('http://d.com/ctr'), "/app/ctr/ctrf1") + self.assertEqual(filter_url('http://d.com/ctr/arg'), "/app/ctr/ctrf1 ['arg']") self.assertEqual(filter_url('http://d.com/app2/arg'), "/app2/default/index ['arg']") self.assertEqual(filter_url('http://d.com/app2/user'), "/app2/default/user") self.assertEqual(filter_url('http://d.com/app2/user/arg'), "/app2/default/user ['arg']") self.assertEqual(filter_url('http://d.com/app2/ctr'), "/app2/ctr/index") @@ -693,11 +712,11 @@ self.assertEqual(get_effective_router('a2').default_application, None) self.assertEqual(get_effective_router('a2').default_controller, "c2") self.assertEqual(get_effective_router('a1').controllers, set(['c1a', 'c1b', 'default', 'static'])) self.assertEqual(get_effective_router('a2').controllers, set()) self.assertEqual(get_effective_router('a3').controllers, set(['c1', 'c2', 'static'])) - self.assertEqual(get_effective_router('a4').functions, set(['f1', 'f2'])) + self.assertEqual(get_effective_router('a4').functions, dict(default=set(['f1', 'f2']))) self.assertEqual(get_effective_router('xx'), None) def test_router_error(self): ''' Test rewrite of HTTP errors Index: gluon/tools.py ================================================================== --- gluon/tools.py +++ gluon/tools.py @@ -14,11 +14,10 @@ import logging import sys import os import re import time -import copy import smtplib import urllib import urllib2 import Cookie import cStringIO @@ -25,19 +24,27 @@ from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string from contenttype import contenttype from storage import Storage, StorageList, Settings, Messages from utils import web2py_uuid -from gluon import * from fileutils import read_file +from gluon import * import serializers -import contrib.simplejson as simplejson +try: + import json as json_parser # try stdlib (Python 2.6) +except ImportError: + try: + import simplejson as json_parser # try external module + except: + import contrib.simplejson as json_parser # fallback to pure-Python module -__all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'PluginManager', 'fetch', 'geocode', 'prettydate'] +__all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', + 'PluginManager', 'fetch', 'geocode', 'prettydate'] +### mind there are two loggers here (logger and crud.settings.logger)! logger = logging.getLogger("web2py") DEFAULT = lambda: None def callback(actions,form,tablename=None): @@ -60,10 +67,19 @@ def call_or_redirect(f,*args): if callable(f): redirect(f(*args)) else: redirect(f) + +def replace_id(url, form): + if url and not url[0] == '/' and url[:4] != 'http': + # this is here for backward compatibility + return URL(url.replace('[id]', str(form.vars.id))) + elif url: + # this allows http://..../%(id)s/%(name)s/etc. + return url % form.vars + return url class Mail(object): """ Class for configuring and sending emails with alternative text / html body, multiple attachments and encryption support @@ -125,26 +141,26 @@ filename=None, content_id=None, content_type=None, encoding='utf-8'): if isinstance(payload, str): - if filename == None: + if filename is None: filename = os.path.basename(payload) payload = read_file(payload, 'rb') else: - if filename == None: + if filename is None: raise Exception('Missing attachment name') payload = payload.read() filename = filename.encode(encoding) - if content_type == None: + if content_type is None: content_type = contenttype(filename) self.my_filename = filename self.my_payload = payload MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1)) self.set_payload(payload) self['Content-Disposition'] = 'attachment; filename="%s"' % filename - if content_id != None: + if not content_id is None: self['Content-Id'] = '<%s>' % content_id.encode(encoding) Encoders.encode_base64(self) def __init__(self, server=None, sender=None, login=None, tls=True): """ @@ -319,36 +335,36 @@ if not isinstance(cc, (list, tuple)): cc = [cc] if bcc: if not isinstance(bcc, (list, tuple)): bcc = [bcc] - if message == None: + if message is None: text = html = None elif isinstance(message, (list, tuple)): text, html = message elif message.strip().startswith(''): text = self.settings.server=='gae' and message or None html = message else: text = message html = None - if text != None or html != None: + if not text is None or not html is None: attachment = MIMEMultipart.MIMEMultipart('alternative') - if text != None: + if not text is None: if isinstance(text, basestring): text = text.decode(encoding).encode('utf-8') else: text = text.read().decode(encoding).encode('utf-8') attachment.attach(MIMEText.MIMEText(text,_charset='utf-8')) - if html != None: + if not html is None: if isinstance(html, basestring): html = html.decode(encoding).encode('utf-8') else: html = html.read().decode(encoding).encode('utf-8') attachment.attach(MIMEText.MIMEText(html, 'html',_charset='utf-8')) payload_in.attach(attachment) - if attachments == None: + if attachments is None: pass elif isinstance(attachments, (list, tuple)): for attachment in attachments: payload_in.attach(attachment) else: @@ -431,11 +447,11 @@ if bcc: rec.extend(bcc) for addr in rec: c.op_keylist_start(addr,0) r = c.op_keylist_next() - if r == None: + if r is None: self.error='No key for [%s]' % addr return False recipients.append(r) try: # make the encryption @@ -546,13 +562,14 @@ payload['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) result = {} try: if self.settings.server == 'logging': - logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\n\n%s\n%s\n' % \ - ('-'*40,self.settings.sender, - ', '.join(to),text or html,'-'*40)) + logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \ + ('-'*40,self.settings.sender, + subject, + ', '.join(to),text or html,'-'*40)) elif self.settings.server == 'gae': xcc = dict() if cc: xcc['cc'] = cc if bcc: @@ -569,19 +586,19 @@ else: result = mail.send_mail(sender=self.settings.sender, to=origTo, subject=subject, body=text, **xcc) else: smtp_args = self.settings.server.split(':') - if self.settings.ssl: + if self.settings.ssl: server = smtplib.SMTP_SSL(*smtp_args) - else: + else: server = smtplib.SMTP(*smtp_args) - if self.settings.tls and not self.settings.ssl: + if self.settings.tls and not self.settings.ssl: server.ehlo() server.starttls() server.ehlo() - if self.settings.login != None: + if not self.settings.login is None: server.login(*self.settings.login.split(':',1)) result = server.sendmail(self.settings.sender, to, payload.as_string()) server.quit() except Exception, e: logger.warn('Mail.send failure:%s' % e) @@ -684,11 +701,11 @@ else: captcha.append(DIV(self.errors['captcha'], _class='error')) return XML(captcha).xml() -def addrow(form,a,b,c,style,_id,position=-1): +def addrow(form, a, b, c, style, _id, position=-1): if style == "divs": form[0].insert(position, DIV(DIV(LABEL(a),_class='w2p_fl'), DIV(b, _class='w2p_fw'), DIV(c, _class='w2p_fc'), _id = _id)) @@ -722,11 +739,11 @@ from contrib.utils import * mail=Mail() mail.settings.server='smtp.gmail.com:587' mail.settings.sender='you@somewhere.com' mail.settings.login='username:password' - auth=Auth(globals(), db) + auth=Auth(db) auth.settings.mailer=mail # auth.settings....=... auth.define_tables() def authentication(): return dict(form=auth()) @@ -787,22 +804,41 @@ ### these are messages that can be customized ... """ - - def url(self, f=None, args=[], vars={}): - return URL(c=self.settings.controller,f=f,args=args,vars=vars) - - def __init__(self, environment=None, db=None, - controller='default', cas_provider = None): - """ - auth=Auth(globals(), db) + @staticmethod + def get_or_create_key(filename=None): + request = current.request + if not filename: + filename = os.path.join(request.folder,'private','auth.key') + if os.path.exists(filename): + key = open(filename,'r').read().strip() + else: + key = web2py_uuid() + open(filename,'w').write(key) + return key + + def url(self, f=None, args=None, vars=None): + if args is None: args=[] + if vars is None: vars={} + return URL(c=self.settings.controller, f=f, args=args, vars=vars) + + def here(self): + return URL(args=current.request.args,vars=current.request.vars) + + def __init__(self, environment=None, db=None, mailer=True, + hmac_key=None, controller='default', cas_provider=None): + """ + auth=Auth(db) - environment is there for legacy but unused (awful) - db has to be the database where to create tables for authentication - + - mailer=Mail(...) or None (no mailed) or True (make a mailer) + - hmac_key can be a hmac_key or hmac_key=Auth.get_or_create_key() + - controller (where is the user action?) + - cas_provider (delegate authentication to the URL, CAS2) """ ## next two lines for backward compatibility if not db and environment and isinstance(environment,DAL): db = environment self.db = db @@ -820,29 +856,35 @@ self.user = None session.auth = None settings = self.settings = Settings() # ## what happens after login? + + self.next = current.request.vars._next + if isinstance(self.next,(list,tuple)): + self.next = self.next[0] # ## what happens after registration? settings.hideerror = False + settings.password_min_length = 4 settings.cas_domains = [request.env.http_host] settings.cas_provider = cas_provider settings.extra_fields = {} settings.actions_disabled = [] settings.reset_password_requires_verification = False settings.registration_requires_verification = False settings.registration_requires_approval = False + settings.login_after_registration = False settings.alternate_requires_registration = False settings.create_user_groups = True settings.controller = controller settings.login_url = self.url('user', args='login') settings.logged_url = self.url('user', args='profile') settings.download_url = self.url('download') - settings.mailer = None + settings.mailer = (mailer==True) and Mail() or mailer settings.login_captcha = None settings.register_captcha = None settings.retrieve_username_captcha = None settings.retrieve_password_captcha = None settings.captcha = None @@ -897,10 +939,11 @@ settings.register_next = self.url('index') settings.register_onvalidation = [] settings.register_onaccept = [] settings.register_fields = None + settings.register_verify_password = True settings.verify_email_next = self.url('user', args='login') settings.verify_email_onaccept = [] settings.profile_next = self.url('index') @@ -917,13 +960,12 @@ settings.change_password_onaccept = [] settings.retrieve_password_onvalidation = [] settings.reset_password_onvalidation = [] - settings.hmac_key = None + settings.hmac_key = hmac_key settings.lock_keys = True - # ## these are messages that can be customized messages = self.messages = Messages(current.T) messages.login_button = 'Login' messages.register_button = 'Register' @@ -950,21 +992,25 @@ messages.invalid_user = 'Invalid user' messages.invalid_password = 'Invalid password' messages.is_empty = "Cannot be empty" messages.mismatched_password = "Password fields don't match" messages.verify_email = \ - 'Click on the link http://...verify_email/%(key)s to verify your email' + 'Click on the link http://' + current.request.env.http_host + \ + URL('default','user',args=['verify_email']) + \ + '/%(key)s to verify your email' messages.verify_email_subject = 'Email verification' messages.username_sent = 'Your username was emailed to you' messages.new_password_sent = 'A new password was emailed to you' messages.password_changed = 'Password changed' messages.retrieve_username = 'Your username is: %(username)s' messages.retrieve_username_subject = 'Username retrieve' messages.retrieve_password = 'Your password is: %(password)s' messages.retrieve_password_subject = 'Password retrieve' messages.reset_password = \ - 'Click on the link http://...reset_password/%(key)s to reset your password' + 'Click on the link http://' + current.request.env.http_host + \ + URL('default','user',args=['reset_password']) + \ + '/%(key)s to reset your password' messages.reset_password_subject = 'Password reset' messages.invalid_reset_password = 'Invalid reset password' messages.profile_updated = 'Profile updated' messages.new_password = 'New password' messages.old_password = 'Old password' @@ -1002,11 +1048,11 @@ messages.label_role = 'Role' messages.label_description = 'Description' messages.label_user_id = 'User ID' messages.label_group_id = 'Group ID' messages.label_name = 'Name' - messages.label_table_name = 'Table name' + messages.label_table_name = 'Object or table name' messages.label_record_id = 'Record ID' messages.label_time_stamp = 'Timestamp' messages.label_client_ip = 'Client IP' messages.label_origin = 'Origin' messages.label_remember_me = "Remember me (for 30 days)" @@ -1079,15 +1125,16 @@ 'impersonate','not_authorized'): return getattr(self,args[0])() elif args[0]=='cas' and not self.settings.cas_provider: if args(1) == 'login': return self.cas_login(version=2) if args(1) == 'validate': return self.cas_validate(version=2) - if args(1) == 'logout': return self.logout() + if args(1) == 'logout': + return self.logout(next=request.vars.service or DEFAULT) else: raise HTTP(404) - def navbar(self,prefix='Welcome',action=None): + def navbar(self, prefix='Welcome', action=None): request = current.request T = current.T if isinstance(prefix,str): prefix = T(prefix) if not action: @@ -1110,11 +1157,11 @@ register=A(T('register'),_href=action+'/register') retrieve_username=A(T('forgot username?'), _href=action+'/retrieve_username') lost_password=A(T('lost password?'), _href=action+'/request_reset_password') - bar = SPAN('[ ',login,' ]',_class='auth_navbar') + bar = SPAN(' [ ',login,' ]',_class='auth_navbar') if not 'register' in self.settings.actions_disabled: bar.insert(2, ' | ') bar.insert(3, register) if 'username' in self.settings.table_user.fields() and \ @@ -1209,11 +1256,13 @@ format='%(first_name)s %(last_name)s (%(id)s)')) table.first_name.requires = \ IS_NOT_EMPTY(error_message=self.messages.is_empty) table.last_name.requires = \ IS_NOT_EMPTY(error_message=self.messages.is_empty) - table[passfield].requires = [CRYPT(key=settings.hmac_key)] + table[passfield].requires = [ + CRYPT(key=settings.hmac_key, + min_length=self.settings.password_min_length)] table.email.requires = \ [IS_EMAIL(error_message=self.messages.invalid_email), IS_NOT_IN_DB(db, table.email)] table.registration_key.default = '' settings.table_user = db[settings.table_user_name] @@ -1270,11 +1319,11 @@ fake_migrate=fake_migrate)) table.group_id.requires = IS_IN_DB(db, '%s.id' % settings.table_group_name, '%(role)s (%(id)s)') table.name.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) - table.table_name.requires = IS_EMPTY_OR(IS_IN_SET(self.db.tables)) + #table.table_name.requires = IS_EMPTY_OR(IS_IN_SET(self.db.tables)) table.record_id.requires = IS_INT_IN_RANGE(0, 10 ** 9) settings.table_permission = db[settings.table_permission_name] if not settings.table_event_name in db.tables: table = db.define_table( settings.table_event_name, @@ -1334,22 +1383,24 @@ urlbase = settings.cas_provider, actions=['login','validate','logout'], maps=maps) - def log_event(self, description, origin='auth'): + def log_event(self, description, vars=None, origin='auth'): """ usage:: auth.log_event(description='this happened', origin='auth') """ - - if self.is_logged_in(): + if not description: + return + elif self.is_logged_in(): user_id = self.user.id else: user_id = None # user unknown - self.settings.table_event.insert(description=description, + vars = vars or {} + self.settings.table_event.insert(description=description % vars, origin=origin, user_id=user_id) def get_or_create_user(self, keys): """ Used for alternate login methods: @@ -1364,11 +1415,10 @@ username = 'username' elif 'email' in table_user.fields(): username = 'email' else: raise SyntaxError, "user must have username or email" - passfield = self.settings.password_field user = self.db(table_user[username] == keys[username]).select().first() keys['registration_key']='' if user: user.update_record(**table_user._filter_fields(keys)) else: @@ -1449,20 +1499,19 @@ if onaccept!=DEFAULT: onaccept(form) allow_access() return self.login(next,onvalidation,cas_onaccept,log) - def cas_validate(self,version=2): + def cas_validate(self, version=2): request = current.request db, table = self.db, self.settings.table_cas current.response.headers['Content-Type']='text' ticket = table(uuid=request.vars.ticket) - url = request.env.path_info.rsplit('/',1)[0] if ticket: # and ticket.created_on>request.now-datetime.timedelta(60): user = self.settings.table_user(ticket.user_id) fullname = user.first_name+' '+user.last_name - if version==1: + if version == 1: raise HTTP(200,'yes\n%s:%s:%s'%(user.id,user.email,fullname)) # assume version 2 username = user.get('username',user.email) raise HTTP(200,'\n'+\ TAG['cas:serviceResponse']( @@ -1470,11 +1519,11 @@ TAG['cas:user'](username), *[TAG['cas:'+field.name](user[field.name]) \ for field in self.settings.table_user \ if field.readable]), **{'_xmlns:cas':'http://www.yale.edu/tp/cas'}).xml()) - if version==1: + if version == 1: raise HTTP(200,'no\n') # assume version 2 raise HTTP(200,'\n'+\ TAG['cas:serviceResponse']( TAG['cas:authenticationFailure']( @@ -1515,14 +1564,22 @@ request = current.request response = current.response session = current.session passfield = self.settings.password_field + try: table_user[passfield].requires[-1].min_length = 0 + except: pass + + ### use session for federated login + if self.next: + session._auth_next = self.next + elif session._auth_next: + self.next = session._auth_next + ### pass + if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.login_next + next = self.next or self.settings.login_next if onvalidation == DEFAULT: onvalidation = self.settings.login_onvalidation if onaccept == DEFAULT: onaccept = self.settings.login_onaccept if log == DEFAULT: @@ -1533,11 +1590,11 @@ # do we use our own login form, or from a central source? if self.settings.login_form == self: form = SQLFORM( table_user, fields=[username, passfield], - hidden=dict(_next=next), + hidden = dict(_next=next), showid=self.settings.showid, submit_button=self.messages.login_button, delete_label=self.messages.delete_label, formstyle=self.settings.formstyle, separator=self.settings.label_separator @@ -1561,11 +1618,12 @@ 'auth_user_remember__row') captcha = self.settings.login_captcha or \ (self.settings.login_captcha!=False and self.settings.captcha) if captcha: - addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') + addrow(form, captcha.label, captcha, captcha.comment, + self.settings.formstyle,'captcha__row') accepted_form = False if form.accepts(request, session, formname='login', dbio=False, onvalidation=onvalidation, @@ -1581,11 +1639,11 @@ response.flash = self.messages.registration_pending return form elif temp_user.registration_key in ('disabled','blocked'): response.flash = self.messages.login_disabled return form - elif temp_user.registration_key!=None and \ + elif not temp_user.registration_key is None and \ temp_user.registration_key.strip(): response.flash = \ self.messages.registration_verifying return form # try alternate logins 1st as these have the @@ -1619,12 +1677,12 @@ # do not store password in db form.vars[passfield] = None user = self.get_or_create_user(form.vars) break if not user: - if self.settings.login_failed_log: - self.log_event(self.settings.login_failed_log % request.post_vars) + self.log_event(self.settings.login_failed_log, + request.post_vars) # invalid login session.flash = self.messages.invalid_login redirect(self.url(args=request.args,vars=request.get_vars)) else: @@ -1632,26 +1690,23 @@ cas = self.settings.login_form cas_user = cas.get_user() if cas_user: cas_user[passfield] = None - user = self.get_or_create_user(table_user._filter_fields(cas_user)) + user = self.get_or_create_user( + table_user._filter_fields(cas_user)) elif hasattr(cas,'login_form'): - return cas.login_form() + return cas.login_form() else: # we need to pass through login again before going on - next = self.url('user',args='login',vars=dict(_next=next)) + next = self.url('user',args='login') redirect(cas.login_url(next)) - # process authenticated users if user: user = Storage(table_user._filter_fields(user, id=True)) - if log: - self.log_event(log % user) - # process authenticated users # user wants to be logged in for longer session.auth = Storage( user = user, last_visit = request.now, @@ -1659,26 +1714,25 @@ remember = request.vars.has_key("remember"), hmac_key = web2py_uuid() ) self.user = user + self.log_event(log, user) session.flash = self.messages.logged_in # how to continue if self.settings.login_form == self: if accepted_form: callback(onaccept,form) - if isinstance(next, (list, tuple)): - # fix issue with 2.6 - next = next[0] - if next and not next[0] == '/' and next[:4] != 'http': - next = self.url(next.replace('[id]', str(form.vars.id))) + next = replace_id(next, form) redirect(next) table_user[username].requires = old_requires return form elif user: callback(onaccept,None) + if next == session._auth_next: + del session._auth_next redirect(next) def logout(self, next=DEFAULT, onlogout=DEFAULT, log=DEFAULT): """ logout and redirects to login @@ -1694,23 +1748,21 @@ onlogout = self.settings.logout_onlogout if onlogout: onlogout(self.user) if log == DEFAULT: log = self.messages.logout_log - if log and self.user: - self.log_event(log % self.user) - + if self.user: + self.log_event(log, self.user) if self.settings.login_form != self: cas = self.settings.login_form cas_user = cas.get_user() if cas_user: next = cas.logout_url(next) current.session.auth = None current.session.flash = self.messages.logged_out - if next: - redirect(next) + redirect(next) def register( self, next=DEFAULT, onvalidation=DEFAULT, @@ -1730,47 +1782,47 @@ response = current.response session = current.session if self.is_logged_in(): redirect(self.settings.logged_url) if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.register_next + next = self.next or self.settings.register_next if onvalidation == DEFAULT: onvalidation = self.settings.register_onvalidation if onaccept == DEFAULT: onaccept = self.settings.register_onaccept if log == DEFAULT: log = self.messages.register_log - passfield = self.settings.password_field - formstyle = self.settings.formstyle + passfield = self.settings.password_field + formstyle = self.settings.formstyle form = SQLFORM(table_user, fields = self.settings.register_fields, - hidden=dict(_next=next), + hidden = dict(_next=next), showid=self.settings.showid, submit_button=self.messages.register_button, delete_label=self.messages.delete_label, formstyle=formstyle, separator=self.settings.label_separator ) - for i, row in enumerate(form[0].components): - item = row.element('input',_name=passfield) - if item: - form.custom.widget.password_two = \ - INPUT(_name="password_two", _type="password", - requires=IS_EXPR('value==%s' % \ - repr(request.vars.get(passfield, None)), - error_message=self.messages.mismatched_password)) - - addrow(form, self.messages.verify_password + ':', - form.custom.widget.password_two, - self.messages.verify_password_comment, - formstyle, - '%s_%s__row' % (table_user, 'password_two'), - position=i+1) - break + if self.settings.register_verify_password: + for i, row in enumerate(form[0].components): + item = row.element('input',_name=passfield) + if item: + form.custom.widget.password_two = \ + INPUT(_name="password_two", _type="password", + requires=IS_EXPR( + 'value==%s' % \ + repr(request.vars.get(passfield, None)), + error_message=self.messages.mismatched_password)) + + addrow(form, self.messages.verify_password + ':', + form.custom.widget.password_two, + self.messages.verify_password_comment, + formstyle, + '%s_%s__row' % (table_user, 'password_two'), + position=i+1) + break captcha = self.settings.register_captcha or self.settings.captcha if captcha: addrow(form, captcha.label, captcha, captcha.comment,self.settings.formstyle, 'captcha__row') table_user.registration_key.default = key = web2py_uuid() @@ -1788,15 +1840,17 @@ % dict(key=key)): self.db.rollback() response.flash = self.messages.unable_send_email return form session.flash = self.messages.email_sent - elif self.settings.registration_requires_approval: + if self.settings.registration_requires_approval: table_user[form.vars.id] = dict(registration_key='pending') session.flash = self.messages.registration_pending - else: - table_user[form.vars.id] = dict(registration_key='') + elif (not self.settings.registration_requires_verification or \ + self.settings.login_after_registration): + if not self.settings.registration_requires_verification: + table_user[form.vars.id] = dict(registration_key='') session.flash = self.messages.registration_successful table_user = self.settings.table_user if 'username' in table_user.fields: username = 'username' else: @@ -1806,19 +1860,16 @@ session.auth = Storage(user=user, last_visit=request.now, expiration=self.settings.expiration, hmac_key = web2py_uuid()) self.user = user session.flash = self.messages.logged_in - if log: - self.log_event(log % form.vars) + self.log_event(log, form.vars) callback(onaccept,form) if not next: next = self.url(args = request.args) - elif isinstance(next, (list, tuple)): ### fix issue with 2.6 - next = next[0] - elif next and not next[0] == '/' and next[:4] != 'http': - next = self.url(next.replace('[id]', str(form.vars.id))) + else: + next = replace_id(next, form) redirect(next) return form def is_logged_in(self): """ @@ -1853,18 +1904,20 @@ user.update_record(registration_key = 'pending') current.session.flash = self.messages.registration_pending else: user.update_record(registration_key = '') current.session.flash = self.messages.email_verified + # make sure session has same user.registrato_key as db record + if current.session.auth and current.session.auth.user: + current.session.auth.user.registration_key = user.registration_key if log == DEFAULT: log = self.messages.verify_email_log if next == DEFAULT: next = self.settings.verify_email_next if onaccept == DEFAULT: onaccept = self.settings.verify_email_onaccept - if log: - self.log_event(log % user) + self.log_event(log, user) callback(onaccept,user) redirect(next) def retrieve_username( self, @@ -1892,13 +1945,11 @@ (self.settings.retrieve_username_captcha!=False and self.settings.captcha) if not self.settings.mailer: response.flash = self.messages.function_disabled return '' if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.retrieve_username_next + next = self.next or self.settings.retrieve_username_next if onvalidation == DEFAULT: onvalidation = self.settings.retrieve_username_onvalidation if onaccept == DEFAULT: onaccept = self.settings.retrieve_username_onaccept if log == DEFAULT: @@ -1906,11 +1957,11 @@ old_requires = table_user.email.requires table_user.email.requires = [IS_IN_DB(self.db, table_user.email, error_message=self.messages.invalid_email)] form = SQLFORM(table_user, fields=['email'], - hidden=dict(_next=next), + hidden = dict(_next=next), showid=self.settings.showid, submit_button=self.messages.submit_button, delete_label=self.messages.delete_label, formstyle=self.settings.formstyle, separator=self.settings.label_separator @@ -1930,19 +1981,16 @@ self.settings.mailer.send(to=form.vars.email, subject=self.messages.retrieve_username_subject, message=self.messages.retrieve_username % dict(username=username)) session.flash = self.messages.email_sent - if log: - self.log_event(log % user) + self.log_event(log, user) callback(onaccept,form) if not next: next = self.url(args = request.args) - elif isinstance(next, (list, tuple)): ### fix issue with 2.6 - next = next[0] - elif next and not next[0] == '/' and next[:4] != 'http': - next = self.url(next.replace('[id]', str(form.vars.id))) + else: + next = replace_id(next, form) redirect(next) table_user.email.requires = old_requires return form def random_password(self): @@ -1978,13 +2026,11 @@ session = current.session if not self.settings.mailer: response.flash = self.messages.function_disabled return '' if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.retrieve_password_next + next = self.next or self.settings.retrieve_password_next if onvalidation == DEFAULT: onvalidation = self.settings.retrieve_password_onvalidation if onaccept == DEFAULT: onaccept = self.settings.retrieve_password_onaccept if log == DEFAULT: @@ -1992,11 +2038,11 @@ old_requires = table_user.email.requires table_user.email.requires = [IS_IN_DB(self.db, table_user.email, error_message=self.messages.invalid_email)] form = SQLFORM(table_user, fields=['email'], - hidden=dict(_next=next), + hidden = dict(_next=next), showid=self.settings.showid, submit_button=self.messages.submit_button, delete_label=self.messages.delete_label, formstyle=self.settings.formstyle, separator=self.settings.label_separator @@ -2024,19 +2070,16 @@ message=self.messages.retrieve_password \ % dict(password=password)): session.flash = self.messages.email_sent else: session.flash = self.messages.unable_to_send_email - if log: - self.log_event(log % user) + self.log_event(log, user) callback(onaccept,form) if not next: next = self.url(args = request.args) - elif isinstance(next, (list, tuple)): ### fix issue with 2.6 - next = next[0] - elif next and not next[0] == '/' and next[:4] != 'http': - next = self.url(next.replace('[id]', str(form.vars.id))) + else: + next = replace_id(next, form) redirect(next) table_user.email.requires = old_requires return form def reset_password( @@ -2058,14 +2101,11 @@ request = current.request # response = current.response session = current.session if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.reset_password_next - + next = self.next or self.settings.reset_password_next try: key = request.vars.key or request.args[-1] t0 = int(key.split('-')[0]) if time.time()-t0 > 60*60*24: raise Exception user = self.db(table_user.reset_password_key == key).select().first() @@ -2081,10 +2121,11 @@ Field('new_password2', 'password', label=self.messages.verify_password, requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), self.messages.mismatched_password)]), submit_button=self.messages.password_reset_button, + hidden = dict(_next=next), formstyle=self.settings.formstyle, separator=self.settings.label_separator ) if form.accepts(request,session,hideerror=self.settings.hideerror): user.update_record(**{passfield:form.vars.new_password, @@ -2115,31 +2156,27 @@ session = current.session captcha = self.settings.retrieve_password_captcha or \ (self.settings.retrieve_password_captcha!=False and self.settings.captcha) if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.request_reset_password_next - + next = self.next or self.settings.request_reset_password_next if not self.settings.mailer: response.flash = self.messages.function_disabled return '' if onvalidation == DEFAULT: onvalidation = self.settings.reset_password_onvalidation if onaccept == DEFAULT: onaccept = self.settings.reset_password_onaccept if log == DEFAULT: log = self.messages.reset_password_log - # old_requires = table_user.email.requires <<< perhaps should be restored table_user.email.requires = [ IS_EMAIL(error_message=self.messages.invalid_email), IS_IN_DB(self.db, table_user.email, error_message=self.messages.invalid_email)] form = SQLFORM(table_user, fields=['email'], - hidden=dict(_next=next), + hidden = dict(_next=next), showid=self.settings.showid, submit_button=self.messages.password_reset_button, delete_label=self.messages.delete_label, formstyle=self.settings.formstyle, separator=self.settings.label_separator @@ -2165,19 +2202,16 @@ dict(key=reset_password_key)): session.flash = self.messages.email_sent user.update_record(reset_password_key=reset_password_key) else: session.flash = self.messages.unable_to_send_email - if log: - self.log_event(log % user) + self.log_event(log, user) callback(onaccept,form) if not next: next = self.url(args = request.args) - elif isinstance(next, (list, tuple)): ### fix issue with 2.6 - next = next[0] - elif next and not next[0] == '/' and next[:4] != 'http': - next = self.url(next.replace('[id]', str(form.vars.id))) + else: + next = replace_id(next, form) redirect(next) # old_requires = table_user.email.requires return form def retrieve_password( @@ -2214,13 +2248,11 @@ s = db(table_user.id == self.user.id) request = current.request session = current.session if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.change_password_next + next = self.next or self.settings.change_password_next if onvalidation == DEFAULT: onvalidation = self.settings.change_password_onvalidation if onaccept == DEFAULT: onaccept = self.settings.change_password_onaccept if log == DEFAULT: @@ -2239,10 +2271,11 @@ Field('new_password2', 'password', label=self.messages.verify_password, requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), self.messages.mismatched_password)]), submit_button=self.messages.password_change_button, + hidden = dict(_next=next), formstyle = self.settings.formstyle, separator=self.settings.label_separator ) if form.accepts(request, session, formname='change_password', @@ -2249,19 +2282,16 @@ onvalidation=onvalidation, hideerror=self.settings.hideerror): d = {passfield: form.vars.new_password} s.update(**d) session.flash = self.messages.password_changed - if log: - self.log_event(log % self.user) + self.log_event(log, self.user) callback(onaccept,form) if not next: next = self.url(args=request.args) - elif isinstance(next, (list, tuple)): ### fix issue with 2.6 - next = next[0] - elif next and not next[0] == '/' and next[:4] != 'http': - next = self.url(next.replace('[id]', str(form.vars.id))) + else: + next = replace_id(next, form) redirect(next) return form def profile( self, @@ -2284,13 +2314,11 @@ passfield = self.settings.password_field self.settings.table_user[passfield].writable = False request = current.request session = current.session if next == DEFAULT: - next = request.get_vars._next \ - or request.post_vars._next \ - or self.settings.profile_next + next = self.next or self.settings.profile_next if onvalidation == DEFAULT: onvalidation = self.settings.profile_onvalidation if onaccept == DEFAULT: onaccept = self.settings.profile_onaccept if log == DEFAULT: @@ -2307,22 +2335,19 @@ formstyle = self.settings.formstyle, separator=self.settings.label_separator ) if form.accepts(request, session, formname='profile', - onvalidation=onvalidation,hideerror=self.settings.hideerror): + onvalidation=onvalidation, hideerror=self.settings.hideerror): self.user.update(table_user._filter_fields(form.vars)) session.flash = self.messages.profile_updated - if log: - self.log_event(log % self.user) + self.log_event(log,self.user) callback(onaccept,form) if not next: next = self.url(args=request.args) - elif isinstance(next, (list, tuple)): ### fix issue with 2.6 - next = next[0] - elif next and not next[0] == '/' and next[:4] != 'http': - next = self.url(next.replace('[id]', str(form.vars.id))) + else: + next = replace_id(next, form) redirect(next) return form def is_impersonating(self): return current.session.auth.impersonator @@ -2358,18 +2383,17 @@ self.user = auth.user if self.settings.login_onaccept: form = Storage(dict(vars=self.user)) self.settings.login_onaccept(form) log = self.messages.impersonate_log - if log: - self.log_event(log % dict(id=current_id,other_id=auth.user.id)) + self.log_event(log,dict(id=current_id, other_id=auth.user.id)) elif user_id in (0, '0') and self.is_impersonating(): session.clear() session.update(cPickle.loads(auth.impersonator)) self.user = session.auth.user if requested_id == DEFAULT and not request.post_vars: - return SQLFORM.factory(Field('user_id','integer')) + return SQLFORM.factory(Field('user_id', 'integer')) return self.user def groups(self): """ displays the groups and their roles for the logged in user @@ -2393,11 +2417,12 @@ def not_authorized(self): """ you can change the view for this page to make it look as you like """ - + if current.request.ajax: + raise HTTP(403,'ACCESS DENIED') return 'ACCESS DENIED' def requires(self, condition): """ decorator that prevents access to action if not logged in @@ -2408,26 +2433,28 @@ def f(*a, **b): if self.settings.allow_basic_login_only and not self.basic(): if current.request.is_restful: raise HTTP(403,"Not authorized") - return call_or_redirect(self.settings.on_failed_authorization) - - if not condition: + return call_or_redirect( + self.settings.on_failed_authorization) + if not self.basic() and not self.is_logged_in(): if current.request.is_restful: raise HTTP(403,"Not authorized") - if not self.basic() and not self.is_logged_in(): - request = current.request - next = URL(r=request,args=request.args, - vars=request.get_vars) - current.session.flash = current.response.flash - return call_or_redirect( - self.settings.on_failed_authentication, - self.settings.login_url + '?_next='+urllib.quote(next)) - else: - current.session.flash = self.messages.access_denied - return call_or_redirect(self.settings.on_failed_authorization) + elif current.request.ajax: + return A('login',_href=self.settings.login_url) + request = current.request + next = self.here() + current.session.flash = current.response.flash + return call_or_redirect( + self.settings.on_failed_authentication, + self.settings.login_url+\ + '?_next='+urllib.quote(next)) + if not condition: + current.session.flash = self.messages.access_denied + return call_or_redirect( + self.settings.on_failed_authorization) return action(*a, **b) f.__doc__ = action.__doc__ f.__name__ = action.__name__ f.__dict__.update(action.__dict__) return f @@ -2436,181 +2463,58 @@ def requires_login(self): """ decorator that prevents access to action if not logged in """ - - def decorator(action): - - def f(*a, **b): - - if self.settings.allow_basic_login_only and not self.basic(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - return call_or_redirect(self.settings.on_failed_authorization) - - if not self.basic() and not self.is_logged_in(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - request = current.request - next = URL(r=request,args=request.args, - vars=request.get_vars) - current.session.flash = current.response.flash - return call_or_redirect( - self.settings.on_failed_authentication, - self.settings.login_url + '?_next='+urllib.quote(next) - ) - return action(*a, **b) - f.__doc__ = action.__doc__ - f.__name__ = action.__name__ - f.__dict__.update(action.__dict__) - return f - - return decorator + return self.requires(self.is_logged_in()) def requires_membership(self, role=None, group_id=None): """ decorator that prevents access to action if not logged in or if user logged in is not a member of group_id. If role is provided instead of group_id then the group_id is calculated. """ - - def decorator(action): - def f(*a, **b): - if self.settings.allow_basic_login_only and not self.basic(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - return call_or_redirect(self.settings.on_failed_authorization) - - if not self.basic() and not self.is_logged_in(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - request = current.request - next = URL(r=request,args=request.args, - vars=request.get_vars) - current.session.flash = current.response.flash - return call_or_redirect( - self.settings.on_failed_authentication, - self.settings.login_url + '?_next='+urllib.quote(next) - ) - if not self.has_membership(group_id=group_id, role=role): - current.session.flash = self.messages.access_denied - return call_or_redirect(self.settings.on_failed_authorization) - return action(*a, **b) - f.__doc__ = action.__doc__ - f.__name__ = action.__name__ - f.__dict__.update(action.__dict__) - return f - - return decorator - - - def requires_permission( - self, - name, - table_name='', - record_id=0, - ): + return self.requires(self.has_membership(group_id=group_id, role=role)) + + def requires_permission(self, name, table_name='', record_id=0): """ decorator that prevents access to action if not logged in or if user logged in is not a member of any group (role) that has 'name' access to 'table_name', 'record_id'. """ - - def decorator(action): - - def f(*a, **b): - if self.settings.allow_basic_login_only and not self.basic(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - return call_or_redirect(self.settings.on_failed_authorization) - - if not self.basic() and not self.is_logged_in(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - request = current.request - next = URL(r=request,args=request.args, - vars=request.get_vars) - current.session.flash = current.response.flash - return call_or_redirect( - self.settings.on_failed_authentication, - self.settings.login_url + '?_next='+urllib.quote(next) - ) - if not self.has_permission(name, table_name, record_id): - current.session.flash = self.messages.access_denied - return call_or_redirect(self.settings.on_failed_authorization) - return action(*a, **b) - f.__doc__ = action.__doc__ - f.__name__ = action.__name__ - f.__dict__.update(action.__dict__) - return f - - return decorator + return self.requires(self.has_permission(name, table_name, record_id)) def requires_signature(self): """ decorator that prevents access to action if not logged in or if user logged in is not a member of group_id. If role is provided instead of group_id then the group_id is calculated. """ - - def decorator(action): - def f(*a, **b): - if self.settings.allow_basic_login_only and not self.basic(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - return call_or_redirect(self.settings.on_failed_authorization) - - if not self.basic() and not self.is_logged_in(): - if current.request.is_restful: - raise HTTP(403,"Not authorized") - request = current.request - next = URL(r=request,args=request.args, - vars=request.get_vars) - current.session.flash = current.response.flash - return call_or_redirect( - self.settings.on_failed_authentication, - self.settings.login_url + '?_next='+urllib.quote(next) - ) - if not URL.verify(current.request,user_signature=True): - current.session.flash = self.messages.access_denied - return call_or_redirect(self.settings.on_failed_authorization) - return action(*a, **b) - f.__doc__ = action.__doc__ - f.__name__ = action.__name__ - f.__dict__.update(action.__dict__) - return f - - return decorator + return self.requires(URL.verify(current.request,user_signature=True)) def add_group(self, role, description=''): """ creates a group associated to a role """ - group_id = self.settings.table_group.insert(role=role, - description=description) - log = self.messages.add_group_log - if log: - self.log_event(log % dict(group_id=group_id, role=role)) + group_id = self.settings.table_group.insert( + role=role, description=description) + self.log_event(self.messages.add_group_log, + dict(group_id=group_id, role=role)) return group_id def del_group(self, group_id): """ deletes a group """ self.db(self.settings.table_group.id == group_id).delete() - self.db(self.settings.table_membership.group_id - == group_id).delete() - self.db(self.settings.table_permission.group_id - == group_id).delete() - log = self.messages.del_group_log - if log: - self.log_event(log % dict(group_id=group_id)) + self.db(self.settings.table_membership.group_id == group_id).delete() + self.db(self.settings.table_permission.group_id == group_id).delete() + self.log_event(self.messages.del_group_log,dict(group_id=group_id)) def id_group(self, role): """ returns the group_id of the group specified by the role """ @@ -2645,20 +2549,18 @@ if self.db((membership.user_id == user_id) & (membership.group_id == group_id)).select(): r = True else: r = False - log = self.messages.has_membership_log - if log: - self.log_event(log % dict(user_id=user_id, - group_id=group_id, check=r)) + self.log_event(self.messages.has_membership_log, + dict(user_id=user_id,group_id=group_id, check=r)) return r def add_membership(self, group_id=None, user_id=None, role=None): """ gives user_id membership of group_id or role - if user_id==None than user_id is that of current logged in user + if user is None than user_id is that of current logged in user """ group_id = group_id or self.id_group(role) try: group_id = int(group_id) @@ -2670,30 +2572,26 @@ record = membership(user_id = user_id,group_id = group_id) if record: return record.id else: id = membership.insert(group_id=group_id, user_id=user_id) - log = self.messages.add_membership_log - if log: - self.log_event(log % dict(user_id=user_id, - group_id=group_id)) + self.log_event(self.messages.add_membership_log, + dict(user_id=user_id, group_id=group_id)) return id def del_membership(self, group_id, user_id=None, role=None): """ revokes membership from group_id to user_id - if user_id==None than user_id is that of current logged in user + if user_id is None than user_id is that of current logged in user """ group_id = group_id or self.id_group(role) if not user_id and self.user: user_id = self.user.id membership = self.settings.table_membership - log = self.messages.del_membership_log - if log: - self.log_event(log % dict(user_id=user_id, - group_id=group_id)) + self.log_event(self.messages.del_membership_log, + dict(user_id=user_id,group_id=group_id)) return self.db(membership.user_id == user_id)(membership.group_id == group_id).delete() def has_permission( @@ -2735,14 +2633,14 @@ for row in rows])) if groups.intersection(groups_required): r = True else: r = False - log = self.messages.has_permission_log - if log and user_id: - self.log_event(log % dict(user_id=user_id, name=name, - table_name=table_name, record_id=record_id)) + if user_id: + self.log_event(self.messages.has_permission_log, + dict(user_id=user_id, name=name, + table_name=table_name, record_id=record_id)) return r def add_permission( self, group_id, @@ -2758,15 +2656,14 @@ if group_id == 0: group_id = self.user_group() id = permission.insert(group_id=group_id, name=name, table_name=str(table_name), record_id=long(record_id)) - log = self.messages.add_permission_log - if log: - self.log_event(log % dict(permission_id=id, group_id=group_id, - name=name, table_name=table_name, - record_id=record_id)) + self.log_event(self.messages.add_permission_log, + dict(permission_id=id, group_id=group_id, + name=name, table_name=table_name, + record_id=record_id)) return id def del_permission( self, group_id, @@ -2777,14 +2674,13 @@ """ revokes group_id 'name' access to 'table_name' and 'record_id' """ permission = self.settings.table_permission - log = self.messages.del_permission_log - if log: - self.log_event(log % dict(group_id=group_id, name=name, - table_name=table_name, record_id=record_id)) + self.log_event(self.messages.del_permission_log, + dict(group_id=group_id, name=name, + table_name=table_name, record_id=record_id)) return self.db(permission.group_id == group_id)(permission.name == name)(permission.table_name == str(table_name))(permission.record_id == long(record_id)).delete() @@ -2798,11 +2694,11 @@ db(auth.accessible_query('read', db.mytable)).select(db.mytable.ALL) """ if not user_id: - user_id = self.user.id + user_id = self.user_id if self.has_permission(name, table, 0, user_id): return table.id > 0 db = self.db membership = self.settings.table_membership permission = self.settings.table_permission @@ -2810,19 +2706,88 @@ (membership.group_id == permission.group_id)\ (permission.name == name)\ (permission.table_name == table)\ ._select(permission.record_id)) + @staticmethod + def archive(form,archive_table=None,current_record='current_record'): + """ + If you have a table (db.mytable) that needs full revision history you can just do:: + + form=crud.update(db.mytable,myrecord,onaccept=auth.archive) + + or + + form=SQLFORM(db.mytable,myrecord).process(onaccept=auth.archive) + + crud.archive will define a new table "mytable_archive" and store the + previous record in the newly created table including a reference + to the current record. + + If you want to access such table you need to define it yourself in a model:: + + db.define_table('mytable_archive', + Field('current_record',db.mytable), + db.mytable) + + Notice such table includes all fields of db.mytable plus one: current_record. + crud.archive does not timestamp the stored record unless your original table + has a fields like:: + + db.define_table(..., + Field('saved_on','datetime', + default=request.now,update=request.now,writable=False), + Field('saved_by',auth.user, + default=auth.user_id,update=auth.user_id,writable=False), + + there is nothing special about these fields since they are filled before + the record is archived. + + If you want to change the archive table name and the name of the reference field + you can do, for example:: + + db.define_table('myhistory', + Field('parent_record',db.mytable), + db.mytable) + + and use it as:: + + form=crud.update(db.mytable,myrecord, + onaccept=lambda form:crud.archive(form, + archive_table=db.myhistory, + current_record='parent_record')) + + """ + old_record = form.record + if not old_record: + return None + table = form.table + if not archive_table: + archive_table_name = '%s_archive' % table + if archive_table_name in table._db: + archive_table = table._db[archive_table_name] + else: + archive_table = table._db.define_table(archive_table_name, + Field(current_record,table), + table) + new_record = {current_record:old_record.id} + for fieldname in archive_table.fields: + if not fieldname in ['id',current_record] and fieldname in old_record: + new_record[fieldname]=old_record[fieldname] + id = archive_table.insert(**new_record) + return id class Crud(object): - def url(self, f=None, args=[], vars={}): + def url(self, f=None, args=None, vars=None): """ this should point to the controller that exposes download and crud """ - return URL(c=self.settings.controller,f=f,args=args,vars=vars) + if args is None: args=[] + if vars is None: vars={} + return URL(c=self.settings.controller, f=f, args=args, vars=vars) def __init__(self, environment, db=None, controller='default'): self.db = db if not db and environment and isinstance(environment,DAL): self.db = environment @@ -2895,13 +2860,13 @@ elif args[0] == 'delete': return self.delete(table, args(2)) else: raise HTTP(404) - def log_event(self, message): + def log_event(self, message, vars): if self.settings.logger: - self.settings.logger.log_event(message, 'crud') + self.settings.logger.log_event(message, vars, origin = 'crud') def has_permission(self, name, table, record=0): if not self.settings.auth: return True try: @@ -2913,74 +2878,14 @@ def tables(self): return TABLE(*[TR(A(name, _href=self.url(args=('select',name)))) \ for name in self.db.tables]) - @staticmethod def archive(form,archive_table=None,current_record='current_record'): - """ - If you have a table (db.mytable) that needs full revision history you can just do:: - - form=crud.update(db.mytable,myrecord,onaccept=crud.archive) - - crud.archive will define a new table "mytable_archive" and store the - previous record in the newly created table including a reference - to the current record. - - If you want to access such table you need to define it yourself in a model:: - - db.define_table('mytable_archive', - Field('current_record',db.mytable), - db.mytable) - - Notice such table includes all fields of db.mytable plus one: current_record. - crud.archive does not timestamp the stored record unless your original table - has a fields like:: - - db.define_table(..., - Field('saved_on','datetime', - default=request.now,update=request.now,writable=False), - Field('saved_by',auth.user, - default=auth.user_id,update=auth.user_id,writable=False), - - there is nothing special about these fields since they are filled before - the record is archived. - - If you want to change the archive table name and the name of the reference field - you can do, for example:: - - db.define_table('myhistory', - Field('parent_record',db.mytable), - db.mytable) - - and use it as:: - - form=crud.update(db.mytable,myrecord, - onaccept=lambda form:crud.archive(form, - archive_table=db.myhistory, - current_record='parent_record')) - - """ - old_record = form.record - if not old_record: - return None - table = form.table - if not archive_table: - archive_table_name = '%s_archive' % table - if archive_table_name in table._db: - archive_table = table._db[archive_table_name] - else: - archive_table = table._db.define_table(archive_table_name, - Field(current_record,table), - table) - new_record = {current_record:old_record.id} - for fieldname in archive_table.fields: - if not fieldname in ['id',current_record] and fieldname in old_record: - new_record[fieldname]=old_record[fieldname] - id = archive_table.insert(**new_record) - return id + return Auth.archive(form,archive_table=archive_table, + current_record=current_record) def update( self, table, record, @@ -3008,19 +2913,18 @@ record_id = record.id except: record_id = record or 0 if record_id and not self.has_permission('update', table, record_id): redirect(self.settings.auth.settings.on_failed_authorization) - if not record_id \ - and not self.has_permission('create', table, record_id): + if not record_id and not self.has_permission('create', table, record_id): redirect(self.settings.auth.settings.on_failed_authorization) request = current.request response = current.response session = current.session if request.extension == 'json' and request.vars.json: - request.vars.update(simplejson.loads(request.vars.json)) + request.vars.update(json_parser.loads(request.vars.json)) if next == DEFAULT: next = request.get_vars._next \ or request.post_vars._next \ or self.settings.update_next if onvalidation == DEFAULT: @@ -3047,25 +2951,22 @@ formstyle=self.settings.formstyle, separator=self.settings.label_separator ) self.accepted = False self.deleted = False - captcha = self.settings.update_captcha or \ - self.settings.captcha + captcha = self.settings.update_captcha or self.settings.captcha if record and captcha: addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') - captcha = self.settings.create_captcha or \ - self.settings.captcha + captcha = self.settings.create_captcha or self.settings.captcha if not record and captcha: addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') if not request.extension in ('html','load'): (_session, _formname) = (None, None) else: - (_session, _formname) = \ - (session, '%s/%s' % (table._tablename, form.record_id)) + (_session, _formname) = (session, '%s/%s' % (table._tablename, form.record_id)) if formname!=DEFAULT: _formname = formname keepvalues = self.settings.keepvalues if request.vars.delete_this_record: keepvalues = False @@ -3076,11 +2977,11 @@ hideerror=self.settings.hideerror, detect_record_change = self.settings.detect_record_change): self.accepted = True response.flash = message if log: - self.log_event(log % form.vars) + self.log_event(log, form.vars) if request.vars.delete_this_record: self.deleted = True message = self.messages.record_deleted callback(ondelete,form,table._tablename) response.flash = message @@ -3088,13 +2989,11 @@ if not request.extension in ('html','load'): raise HTTP(200, 'RECORD CREATED/UPDATED') if isinstance(next, (list, tuple)): ### fix issue with 2.6 next = next[0] if next: # Only redirect when explicit - if next[0] != '/' and next[:4] != 'http': - next = URL(r=request, - f=next.replace('[id]', str(form.vars.id))) + next = replace_id(next, form) session.flash = response.flash redirect(next) elif not request.extension in ('html','load'): raise HTTP(401) return form @@ -3188,22 +3087,20 @@ if record: callback(self.settings.delete_onvalidation,record) del table[record_id] callback(self.settings.delete_onaccept,record,table._tablename) session.flash = message - if next: # Only redirect when explicit - redirect(next) + redirect(next) def rows( self, table, query=None, fields=None, orderby=None, limitby=None, ): - request = current.request if not (isinstance(table, self.db.Table) or table in self.db.tables): raise HTTP(404) if not self.has_permission('select', table): redirect(self.settings.auth.settings.on_failed_authorization) #if record_id and not self.has_permission('select', table): @@ -3223,13 +3120,14 @@ table, query=None, fields=None, orderby=None, limitby=None, - headers={}, + headers=None, **attr ): + headers = headers or {} rows = self.rows(table,query,fields,orderby,limitby) if not rows: return None # Nicer than an empty table. if not 'upload' in attr: attr['upload'] = self.url('download') @@ -3287,11 +3185,10 @@ else: return lambda row: value in row[field.name][format] except: return None - def search(self, *tables, **args): """ Creates a search form and its results for a table Example usage: form, results = crud.search(db.test, @@ -3312,10 +3209,17 @@ attributes = {} for key in ('orderby','groupby','left','distinct','limitby','cache'): if key in args: attributes[key]=args[key] tbl = TABLE() selected = []; refsearch = []; results = [] + showall = args.get('showall', False) + if showall: + selected = fields + chkall = args.get('chkall', False) + if chkall: + for f in fields: + request.vars['chk%s'%f] = 'on' ops = args.get('queries', []) zero = args.get('zero', '') if not ops: ops = ['equals', 'not equal', 'greater than', 'less than', 'starts with', @@ -3367,24 +3271,25 @@ return form, results urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor())) -def fetch(url, data=None, headers={}, +def fetch(url, data=None, headers=None, cookie=Cookie.SimpleCookie(), user_agent='Mozilla/5.0'): - if data != None: + headers = headers or {} + if not data is None: data = urllib.urlencode(data) if user_agent: headers['User-agent'] = user_agent headers['Cookie'] = ' '.join(['%s=%s;'%(c.key,c.value) for c in cookie.values()]) try: from google.appengine.api import urlfetch except ImportError: req = urllib2.Request(url, data, headers) html = urllib2.urlopen(req).read() else: - method = ((data==None) and urlfetch.GET) or urlfetch.POST + method = ((data is None) and urlfetch.GET) or urlfetch.POST while url is not None: response = urlfetch.fetch(url=url, payload=data, method=method, headers=headers, allow_truncated=False,follow_redirects=False, deadline=10) @@ -3462,11 +3367,11 @@ def run(self, f): """ example:: - service = Service(globals()) + service = Service() @service.run def myfunction(a, b): return a + b def call(): return service() @@ -3481,11 +3386,11 @@ def csv(self, f): """ example:: - service = Service(globals()) + service = Service() @service.csv def myfunction(a, b): return a + b def call(): return service() @@ -3500,11 +3405,11 @@ def xml(self, f): """ example:: - service = Service(globals()) + service = Service() @service.xml def myfunction(a, b): return a + b def call(): return service() @@ -3519,11 +3424,11 @@ def rss(self, f): """ example:: - service = Service(globals()) + service = Service() @service.rss def myfunction(): return dict(title=..., link=..., description=..., created_on=..., entries=[dict(title=..., link=..., description=..., created_on=...]) @@ -3540,11 +3445,11 @@ def json(self, f): """ example:: - service = Service(globals()) + service = Service() @service.json def myfunction(a, b): return [{a: b}] def call(): return service() @@ -3559,11 +3464,11 @@ def jsonrpc(self, f): """ example:: - service = Service(globals()) + service = Service() @service.jsonrpc def myfunction(a, b): return a + b def call(): return service() @@ -3578,11 +3483,11 @@ def xmlrpc(self, f): """ example:: - service = Service(globals()) + service = Service() @service.xmlrpc def myfunction(a, b): return a + b def call(): return service() @@ -3597,11 +3502,11 @@ def amfrpc(self, f): """ example:: - service = Service(globals()) + service = Service() @service.amfrpc def myfunction(a, b): return a + b def call(): return service() @@ -3616,11 +3521,11 @@ def amfrpc3(self, domain='default'): """ example:: - service = Service(globals()) + service = Service() @service.amfrpc3('domain') def myfunction(a, b): return a + b def call(): return service() @@ -3643,11 +3548,11 @@ def soap(self, name=None, returns=None, args=None,doc=None): """ example:: - service = Service(globals()) + service = Service() @service.soap('MyFunction',returns={'result':int},args={'a':int,'b':int,}) def myfunction(a, b): return a + b def call(): return service() @@ -3687,11 +3592,11 @@ def none_exception(value): if isinstance(value, unicode): return value.encode('utf8') if hasattr(value, 'isoformat'): return value.isoformat()[:19].replace('T', ' ') - if value == None: + if value is None: return '' return value if args and args[0] in self.run_procedures: r = universal_caller(self.run_procedures[args[0]], *args[1:], **dict(request.vars)) @@ -3741,11 +3646,11 @@ return serializers.rss(feed) def serve_json(self, args=None): request = current.request response = current.response - response.headers['Content-Type'] = 'text/x-json' + response.headers['Content-Type'] = 'application/json; charset=utf-8' if not args: args = request.args d = dict(request.vars) if args and args[0] in self.json_procedures: s = universal_caller(self.json_procedures[args[0]],*args[1:],**d) @@ -3757,25 +3662,25 @@ class JsonRpcException(Exception): def __init__(self,code,info): self.code,self.info = code,info def serve_jsonrpc(self): - import contrib.simplejson as simplejson def return_response(id, result): return serializers.json({'version': '1.1', 'id': id, 'result': result, 'error': None}) - def return_error(id, code, message): return serializers.json({'id': id, 'version': '1.1', 'error': {'name': 'JSONRPCError', 'code': code, 'message': message} }) request = current.request + response = current.response + response.headers['Content-Type'] = 'application/json; charset=utf-8' methods = self.jsonrpc_procedures - data = simplejson.loads(request.body.read()) + data = json_parser.loads(request.body.read()) id, method, params = data['id'], data['method'], data.get('params','') if not method in methods: return return_error(id, 100, 'method "%s" does not exist' % method) try: s = methods[method](*params) @@ -3891,17 +3796,16 @@ return {'body': body} def __call__(self): """ register services with: - service = Service(globals()) + service = Service() @service.run @service.rss @service.json @service.jsonrpc @service.xmlrpc - @service.jsonrpc @service.amfrpc @service.amfrpc3('domain') @service.soap('Method', returns={'Result':int}, args={'a':int,'b':int,}) expose services with @@ -4097,11 +4001,12 @@ def __init__(self,plugin=None,**defaults): if not plugin: self.__dict__.clear() settings = self.__getattr__(plugin) settings.installed = True - [settings.update({key:value}) for key,value in defaults.items() if not key in settings] + [settings.update({key:value}) for key,value in defaults.items() \ + if not key in settings] def __getattr__(self, key): if not key in self.__dict__: self.__dict__[key] = Storage() return self.__dict__[key] def keys(self): @@ -4110,6 +4015,5 @@ return key in self.__dict__ if __name__ == '__main__': import doctest doctest.testmod() - ADDED gluon/tools.pyc Index: gluon/tools.pyc ================================================================== --- gluon/tools.pyc +++ gluon/tools.pyc cannot compute difference between binary files Index: gluon/utils.py ================================================================== --- gluon/utils.py +++ gluon/utils.py @@ -122,6 +122,8 @@ except NotImplementedError: pass ## xor bytes with constant ctokens bytes = ''.join(chr(c ^ ctokens[i]) for i,c in enumerate(bytes)) return str(uuid.UUID(bytes=bytes, version=4)) + + ADDED gluon/utils.pyc Index: gluon/utils.pyc ================================================================== --- gluon/utils.pyc +++ gluon/utils.pyc cannot compute difference between binary files Index: gluon/validators.py ================================================================== --- gluon/validators.py +++ gluon/validators.py @@ -17,11 +17,11 @@ import urllib import struct import decimal import unicodedata from cStringIO import StringIO -from utils import simple_hash, hmac_hash +from utils import simple_hash, hmac_hash, web2py_uuid __all__ = [ 'CLEANUP', 'CRYPT', 'IS_ALPHANUMERIC', @@ -119,24 +119,45 @@ the argument of IS_MATCH is a regular expression:: >>> IS_MATCH('.+')('hello') ('hello', None) + + >>> IS_MATCH('hell')('hello') + ('hello', 'invalid expression') + + >>> IS_MATCH('hell.*', strict=False)('hello') + ('hello', None) + + >>> IS_MATCH('hello')('shello') + ('shello', 'invalid expression') + + >>> IS_MATCH('hello', search=True)('shello') + ('hello', None) + + >>> IS_MATCH('hello', search=True, strict=False)('shellox') + ('hello', None) + + >>> IS_MATCH('.*hello.*', search=True, strict=False)('shellox') + ('shellox', None) >>> IS_MATCH('.+')('') ('', 'invalid expression') """ - def __init__(self, expression, error_message='invalid expression', strict=True): + def __init__(self, expression, error_message='invalid expression', strict=True, search=False): if strict: if not expression.endswith('$'): expression = '(%s)$' % expression + if not search: + if not expression.startswith('^'): + expression = '^(%s)' % expression self.regex = re.compile(expression) self.error_message = error_message def __call__(self, value): - match = self.regex.match(value) + match = self.regex.search(value) if match: return (match.group(), None) return (value, translate(self.error_message)) @@ -316,11 +337,11 @@ items = [(k, k) for (i, k) in enumerate(self.theset)] else: items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)] if self.sort: items.sort(options_sorter) - if zero and self.zero != None and not self.multiple: + if zero and not self.zero is None and not self.multiple: items.insert(0,('',self.zero)) return items def __call__(self, value): if self.multiple: @@ -333,11 +354,11 @@ values = [] else: values = [value] failures = [x for x in values if not x in self.theset] if failures and self.theset: - if self.multiple and (value == None or value == ''): + if self.multiple and (value is None or value == ''): return ([], None) return (value, translate(self.error_message)) if self.multiple: if isinstance(self.multiple,(tuple,list)) and \ not self.multiple[0]<=len(values)0: + value = web2py_uuid() + elif len(value) 0)): ok = False - if not (self.is_automatic == None or self.is_automatic == \ + if not (self.is_automatic is None or self.is_automatic == \ (self.automatic[0] <= number <= self.automatic[1])): ok = False if ok: return (value, None) return (value, translate(self.error_message)) if __name__ == '__main__': import doctest doctest.testmod() + + ADDED gluon/validators.pyc Index: gluon/validators.pyc ================================================================== --- gluon/validators.pyc +++ gluon/validators.pyc cannot compute difference between binary files Index: gluon/widget.py ================================================================== --- gluon/widget.py +++ gluon/widget.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python +#!/-usr/bin/env python # -*- coding: utf-8 -*- """ This file is part of the web2py Web Framework Copyrighted by Massimo Di Pierro @@ -17,11 +17,10 @@ import os import socket import signal import math import logging - import newcron import main from fileutils import w2p_pack, read_file, write_file from shell import run, test @@ -110,15 +109,17 @@ width=500, height=300) canvas.pack() root.update() - img = Tkinter.PhotoImage(file='splashlogo.gif') - pnl = Tkinter.Label(canvas, image=img, background='white', bd=0) - pnl.pack(side='top', fill='both', expand='yes') - # Prevent garbage collection of img - pnl.image=img + logo = 'splashlogo.gif' + if os.path.exists(logo): + img = Tkinter.PhotoImage(file=logo) + pnl = Tkinter.Label(canvas, image=img, background='white', bd=0) + pnl.pack(side='top', fill='both', expand='yes') + # Prevent garbage collection of img + pnl.image=img def add_label(text='Change Me', font_size=12, foreground='#195866', height=1): return Tkinter.Label( master=canvas, width=250, @@ -219,11 +220,11 @@ column=0, sticky=sticky) self.password = Tkinter.Entry(self.root, show='*') self.password.bind('', lambda e: self.start()) - self.password.focus_force() + self.password.focus_force() self.password.grid(row=2, column=1, sticky=sticky) # Prepare the canvas self.canvas = Tkinter.Canvas(self.root, width=300, @@ -319,11 +320,11 @@ self.tb.Destroy() except: pass self.root.destroy() - sys.exit() + sys.exit(0) def error(self, message): """ Show error message """ tkMessageBox.showerror('web2py start server', message) @@ -486,10 +487,16 @@ '--ssl_private_key', default='', dest='ssl_private_key', help='file that contains ssl private key') + parser.add_option('--ca-cert', + action='store', + dest='ssl_ca_certificate', + default=None, + help='Use this file containing the CA certificate to validate X509 certificates from clients') + parser.add_option('-d', '--pid_filename', default='httpserver.pid', dest='pid_filename', help='file to store the pid of the server') @@ -617,10 +624,19 @@ '--run', dest='run', metavar='PYTHON_FILE', default='', help=msg) + + msg = 'run scheduled tasks for the specified apps' + msg += '-K app1,app2,app3' + msg += 'requires a scheduler defined in the models' + parser.add_option('-K', + '--scheduler', + dest='scheduler', + default=None, + help=msg) msg = 'run doctests in web2py environment; ' +\ 'TEST_PATH like a/c/f (c,f optional)' parser.add_option('-T', '--test', @@ -701,11 +717,12 @@ action='store_true', default=False, dest='nobanner', help='Do not print header banner') - msg = 'listen on multiple addresses: "ip:port:cert:key;ip2:port2:cert2:key2;..." (:cert:key optional; no spaces)' + + msg = 'listen on multiple addresses: "ip:port:cert:key:ca_cert;ip2:port2:cert2:key2:ca_cert2;..." (:cert:key optional; no spaces)' parser.add_option('--interfaces', action='store', dest='interfaces', default=None, help=msg) @@ -734,18 +751,20 @@ options.nocron = True # don't start cron jobs options.plain = True # cronjobs use a plain shell options.folder = os.path.abspath(options.folder) - # accept --interfaces in the form "ip:port:cert:key;ip2:port2;ip3:port3:cert3:key3" + # accept --interfaces in the form + # "ip:port:cert:key;ip2:port2;ip3:port3:cert3:key3" # (no spaces; optional cert:key indicate SSL) - # if isinstance(options.interfaces, str): - options.interfaces = [interface.split(':') for interface in options.interfaces.split(';')] + options.interfaces = [ + interface.split(':') for interface in options.interfaces.split(';')] for interface in options.interfaces: interface[1] = int(interface[1]) # numeric port - options.interfaces = [tuple(interface) for interface in options.interfaces] + options.interfaces = [ + tuple(interface) for interface in options.interfaces] if options.numthreads is not None and options.minthreads is None: options.minthreads = options.numthreads # legacy if not options.cronjob: @@ -761,11 +780,36 @@ msg = "New installation: unable to create welcome.w2p file" sys.stderr.write(msg) return (options, args) +def start_schedulers(options): + apps = [app.strip() for app in options.scheduler.split(',')] + try: + from multiprocessing import Process + except: + sys.stderr.write('Sorry, -K only supported for python 2.6-2.7\n') + return + processes = [] + code = "from gluon import current; current._scheduler.loop()" + for app in apps: + print 'starting scheduler for "%s"...' % app + args = (app,True,True,None,False,code) + logging.getLogger().setLevel(logging.DEBUG) + p = Process(target=run, args=args) + processes.append(p) + print "Currently running %s scheduler processes" % (len(processes)) + p.start() + print "Processes started" + for p in processes: + try: + p.join() + except KeyboardInterrupt: + p.terminate() + p.join() + def start(cron=True): """ Start server """ # ## get command line arguments @@ -798,14 +842,22 @@ # ## if -T run doctests (no cron) if hasattr(options,'test') and options.test: test(options.test, verbose=options.verbose) return + + # ## if -K + if options.scheduler: + try: + start_schedulers(options) + except KeyboardInterrupt: + pass + return # ## if -S start interactive shell (also no cron) if options.shell: - if options.args!=None: + if not options.args is None: sys.argv[:] = options.args run(options.shell, plain=options.plain, bpython=options.bpython, import_models=options.import_models, startfile=options.run) return @@ -903,10 +955,11 @@ pid_filename=options.pid_filename, log_filename=options.log_filename, profiler_filename=options.profiler_filename, ssl_certificate=options.ssl_certificate, ssl_private_key=options.ssl_private_key, + ssl_ca_certificate=options.ssl_ca_certificate, min_threads=options.minthreads, max_threads=options.maxthreads, server_name=options.server_name, request_queue_size=options.request_queue_size, timeout=options.timeout, @@ -917,6 +970,8 @@ try: server.start() except KeyboardInterrupt: server.stop() logging.shutdown() + + ADDED gluon/widget.pyc Index: gluon/widget.pyc ================================================================== --- gluon/widget.pyc +++ gluon/widget.pyc cannot compute difference between binary files Index: gluon/winservice.py ================================================================== --- gluon/winservice.py +++ gluon/winservice.py @@ -159,6 +159,8 @@ serviceClassString=classstring, argv=argv) if __name__ == '__main__': web2py_windows_service_handler() + + Index: gluon/xmlrpc.py ================================================================== --- gluon/xmlrpc.py +++ gluon/xmlrpc.py @@ -17,6 +17,8 @@ dispatcher.register_function(method) dispatcher.register_introspection_functions() response.headers['Content-Type'] = 'text/xml' dispatch = getattr(dispatcher, '_dispatch', None) return dispatcher._marshaled_dispatch(request.body.read(), dispatch) + + ADDED gluon/xmlrpc.pyc Index: gluon/xmlrpc.pyc ================================================================== --- gluon/xmlrpc.pyc +++ gluon/xmlrpc.pyc cannot compute difference between binary files ADDED isapiwsgihandler.py Index: isapiwsgihandler.py ================================================================== --- isapiwsgihandler.py +++ isapiwsgihandler.py @@ -0,0 +1,36 @@ +""" +web2py handler for isapi-wsgi for IIS. Requires: +http://code.google.com/p/isapi-wsgi/ +""" +# The entry point for the ISAPI extension. +def __ExtensionFactory__(): + import os + import sys + path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(path) + sys.path = [path]+[p for p in sys.path if not p==path] + import gluon.main + import isapi_wsgi + application=gluon.main.wsgibase + return isapi_wsgi.ISAPIThreadPoolHandler(application) + +# ISAPI installation: +if __name__=='__main__': + import sys + if len(sys.argv)<2: + print "USAGE: python isapiwsgihandler.py install --server=Sitename" + sys.exit(0) + from isapi.install import ISAPIParameters + from isapi.install import ScriptMapParams + from isapi.install import VirtualDirParameters + from isapi.install import HandleCommandLine + + params = ISAPIParameters() + sm = [ ScriptMapParams(Extension="*", Flags=0) ] + vd = VirtualDirParameters(Name="appname", + Description = "Web2py in Python", + ScriptMaps = sm, + ScriptMapUpdate = "replace") + params.VirtualDirs = [vd] + HandleCommandLine(params) + Index: modpythonhandler.py ================================================================== --- modpythonhandler.py +++ modpythonhandler.py @@ -220,5 +220,6 @@ def handler(req): """ Execute the gluon app """ Handler(req).run(gluon.main.wsgibase) return apache.OK + Index: options_std.py ================================================================== --- options_std.py +++ options_std.py @@ -29,5 +29,6 @@ timeout = 30 shutdown_timeout = 5 folder = os.getcwd() extcron = None nocron = None + Index: router.example.py ================================================================== --- router.example.py +++ router.example.py @@ -12,39 +12,42 @@ # # Router members: # # default_application: default application name # applications: list of all recognized applications, or 'ALL' to use all currently installed applications -# Names in applications are always treated as an application names when they appear first in an incoming URL. -# Set applications=None to disable the removal of application names from outgoing URLs. +# Names in applications are always treated as an application names when they appear first in an incoming URL. +# Set applications=None to disable the removal of application names from outgoing URLs. # domains: optional dict mapping domain names to application names -# The domain name can include a port number: domain.com:8080 -# The application name can include a controller: appx/ctlrx -# Example: -# domains = { "domain.com" : "app", +# The domain name can include a port number: domain.com:8080 +# The application name can include a controller: appx/ctlrx +# or a controller and a function: appx/ctlrx/fcnx +# Example: +# domains = { "domain.com" : "app", # "x.domain.com" : "appx", -# }, +# }, # path_prefix: a path fragment that is prefixed to all outgoing URLs and stripped from all incoming URLs # # Note: default_application, applications, domains & path_prefix are permitted only in the BASE router, # and domain makes sense only in an application-specific router. # The remaining members can appear in the BASE router (as defaults for all applications) # or in application-specific routers. # # default_controller: name of default controller -# default_function: name of default function (in all controllers) +# default_function: name of default function (in all controllers) or dictionary of default functions +# by controller # controllers: list of valid controllers in selected app # or "DEFAULT" to use all controllers in the selected app plus 'static' # or None to disable controller-name removal. # Names in controllers are always treated as controller names when they appear in an incoming URL after -# the (optional) application and language names. -# functions: list of valid functions in the default controller (default None) -# If present, the default function name will be omitted when the controller is the default controller -# and the first arg does not create an ambiguity. +# the (optional) application and language names. +# functions: list of valid functions in the default controller (default None) or dictionary of valid +# functions by controller. +# If present, the default function name will be omitted when the controller is the default controller +# and the first arg does not create an ambiguity. # languages: list of all supported languages -# Names in languages are always treated as language names when they appear in an incoming URL after -# the (optional) application name. +# Names in languages are always treated as language names when they appear in an incoming URL after +# the (optional) application name. # default_language # The language code (for example: en, it-it) optionally appears in the URL following # the application (which may be omitted). For incoming URLs, the code is copied to # request.language; for outgoing URLs it is taken from request.language. # If languages=None, language support is disabled. @@ -52,20 +55,20 @@ # root_static: list of static files accessed from root (by default, favicon.ico & robots.txt) # (mapped to the default application's static/ directory) # Each default (including domain-mapped) application has its own root-static files. # domain: the domain that maps to this application (alternative to using domains in the BASE router) # exclusive_domain: If True (default is False), an exception is raised if an attempt is made to generate -# an outgoing URL with a different application without providing an explicit host. -# map_hyphen: If True (default is False), hyphens in incoming /a/c/f fields are converted -# to underscores, and back to hyphens in outgoing URLs. -# Language, args and the query string are not affected. -# map_static: By default, the default application is not stripped from static URLs. +# an outgoing URL with a different application without providing an explicit host. +# map_hyphen: If True (default is False), hyphens in incoming /a/c/f fields are converted +# to underscores, and back to hyphens in outgoing URLs. +# Language, args and the query string are not affected. +# map_static: By default, the default application is not stripped from static URLs. # Set map_static=True to override this policy. # acfe_match: regex for valid application, controller, function, extension /a/c/f.e # file_match: regex for valid file (used for static file names) # args_match: regex for valid args -# This validation provides a measure of security. +# This validation provides a measure of security. # If it is changed, the application perform its own validation. # # # The built-in default router supplies default values (undefined members are None): # @@ -181,11 +184,11 @@ '/fcn?query' >>> filter_url('http://domain.com/welcome/default/fcn#anchor', out=True) '/fcn#anchor' >>> filter_url('http://domain.com/welcome/default/fcn?query#anchor', out=True) '/fcn?query#anchor' - + >>> filter_err(200) 200 >>> filter_err(399) 399 >>> filter_err(400) @@ -194,5 +197,6 @@ pass if __name__ == '__main__': import doctest doctest.testmod() + Index: routes.example.py ================================================================== --- routes.example.py +++ routes.example.py @@ -161,5 +161,6 @@ pass if __name__ == '__main__': import doctest doctest.testmod() + Index: scgihandler.py ================================================================== --- scgihandler.py +++ scgihandler.py @@ -8,13 +8,13 @@ from fcgihandler.py to support SCGI fcgihandler has the following copyright: " This file is part of the web2py Web Framework Copyrighted by Massimo Di Pierro - License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) + License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) " - + This is a handler for lighttpd+scgi This file has to be in the PYTHONPATH Put something like this in the lighttpd.conf file: server.document-root="/var/www/web2py/" @@ -67,5 +67,6 @@ global_settings.web2py_crontype = 'soft' # uncomment one of the two rows below depending on the SCGIWSGI server installed #scgi.serve_application(application, '', 4000).run() SCGIServer(application, port=4000).run() + ADDED scripts/cleanjs.py Index: scripts/cleanjs.py ================================================================== --- scripts/cleanjs.py +++ scripts/cleanjs.py @@ -0,0 +1,17 @@ +import re + +def cleanjs(text): + text = re.sub('\s*}\s*','\n}\n',text) + text = re.sub('\s*{\s*',' {\n',text) + text = re.sub('\s*;\s*',';\n',text) + text = re.sub('\s*,\s*',', ',text) + text = re.sub('\s*(?P[\+\-\*/\=]+)\s*',' \g ',text) + lines = text.split('\n') + text='' + indent=0 + for line in lines: + rline=line.strip() + if rline: + pass + return text + ADDED scripts/dbsessions2trash.py Index: scripts/dbsessions2trash.py ================================================================== --- scripts/dbsessions2trash.py +++ scripts/dbsessions2trash.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from datetime import datetime +from time import mktime +from time import sleep +from time import time + +DB_URI = 'sqlite://sessions.sqlite' +EXPIRATION_MINUTES = 60 +SLEEP_MINUTES = 5 + +while 1: # Infinite loop + now = time() # get current Unix timestamp + + for row in db().select(db.web2py_session_welcome.ALL): + t = row.modified_datetime + # Convert to a Unix timestamp + t = mktime(t.timetuple())+1e-6*t.microsecond + if now - t > EXPIRATION_MINUTES * 60: + del db.web2py_session_welcome[row.id] + + db.commit() # Write changes to database + + sleep(SLEEP_MINUTES * 60) + ADDED scripts/make_min_web2py.py Index: scripts/make_min_web2py.py ================================================================== --- scripts/make_min_web2py.py +++ scripts/make_min_web2py.py @@ -0,0 +1,79 @@ +USAGE = """ +from web2py main folder +python scripts/make_min_web2py.py /path/to/minweb2py + +it will mkdir minweb2py and build a minimal web2py installation +- no admin, no examples, one line welcome +- no scripts +- drops same rarely used contrib modules +- more modules could be dropped but minimal difference +""" + +# files to include from top level folder (default.py will be rebuilt) +REQUIRED = """ +VERSION +web2py.py +fcgihandler.py +gaehandler.py +wsgihandler.py +anyserver.py +applications/__init__.py +applications/welcome/controllers/default.py +""" + +# files and folders to exclude from gluon folder (comment with # if needed) +IGNORED = """ +gluon/contrib/comet_messaging.py +gluon/contrib/feedparser.py +gluon/contrib/generics.py +gluon/contrib/gql.py +gluon/contrib/populate.py +gluon/contrib/sms_utils.py +gluon/contrib/spreadsheet.py +gluon/tests/ +gluon/contrib/markdown/ +gluon/contrib/pyfpdf/ +gluon/contrib/pymysql/ +gluon/contrib/pyrtf/ +gluon/contrib/pysimplesoap/ +""" + +import sys, os, shutil, glob + +def main(): + if len(sys.argv)<2: + print USAGE + + # make target folder + target = sys.argv[1] + os.mkdir(target) + + # make a list of all files to include + files = [x.strip() for x in REQUIRED.split('\n') \ + if x and not x[0]=='#'] + ignore = [x.strip() for x in IGNORED.split('\n') \ + if x and not x[0]=='#'] + def accept(filename): + for p in ignore: + if filename.startswith(p): + return False + return True + pattern = 'gluon/*.py' + while True: + newfiles = [x for x in glob.glob(pattern) if accept(x)] + if not newfiles: break + files += newfiles + pattern = pattern[:-3]+'/*.py' + # copy all files, make missing folder, build default.py + files.sort() + for f in files: + dirs = f.split(os.path.sep) + for i in range(1,len(dirs)): + try: os.mkdir(target+'/'+os.path.join(*dirs[:i])) + except OSError: pass + if f=='applications/welcome/controllers/default.py': + open(target+'/'+f,'w').write('def index(): return "hello"\n') + else: + shutil.copyfile(f,target+'/'+f) + +if __name__=='__main__': main() ADDED scripts/sessions2trash.py Index: scripts/sessions2trash.py ================================================================== --- scripts/sessions2trash.py +++ scripts/sessions2trash.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +sessions2trash.py + +Run this script in a web2py environment shell e.g. python web2py.py -S app +If models are loaded (-M option) auth.settings.expiration is assumed +for sessions without an expiration. If models are not loaded, sessions older +than 60 minutes are removed. Use the --expiration option to override these +values. + +Typical usage: + + # Delete expired sessions every 5 minutes + nohup python web2py.py -S app -M -R scripts/sessions2trash.py & + + # Delete sessions older than 60 minutes regardless of expiration, + # with verbose output, then exit. + python web2py.py -S app -M -R scripts/sessions2trash.py -A -o -x 3600 -f -v + + # Delete all sessions regardless of expiry and exit. + python web2py.py -S app -M -R scripts/sessions2trash.py -A -o -x 0 +""" + +from gluon.storage import Storage +from optparse import OptionParser +import cPickle +import datetime +import os +import stat +import time + +EXPIRATION_MINUTES = 60 +SLEEP_MINUTES = 5 +VERSION = 0.3 + + +class SessionSet(object): + """Class representing a set of sessions""" + + def __init__(self, expiration, force, verbose): + self.expiration = expiration + self.force = force + self.verbose = verbose + + def get(self): + """Get session files/records.""" + raise NotImplementedError + + def trash(self): + """Trash expired sessions.""" + now = datetime.datetime.now() + for item in self.get(): + status = 'OK' + last_visit = item.last_visit_default() + + try: + session = item.get() + if session.auth: + if session.auth.expiration and not self.force: + self.expiration = session.auth.expiration + if session.auth.last_visit: + last_visit = session.auth.last_visit + except: + pass + + age = 0 + if last_visit: + age = total_seconds(now - last_visit) + + if age > self.expiration or not self.expiration: + item.delete() + status = 'trashed' + + if self.verbose > 1: + print 'key: %s' % str(item) + print 'expiration: %s seconds' % self.expiration + print 'last visit: %s' % str(last_visit) + print 'age: %s seconds' % age + print 'status: %s' % status + print '' + elif self.verbose > 0: + print('%s %s' % (str(item), status)) + + +class SessionSetDb(SessionSet): + """Class representing a set of sessions stored in database""" + + def __init__(self, expiration, force, verbose): + SessionSet.__init__(self, expiration, force, verbose) + + def get(self): + """Return list of SessionDb instances for existing sessions.""" + sessions = [] + tablename = 'web2py_session' + if request.application: + tablename = 'web2py_session_' + request.application + if tablename in db: + for row in db(db[tablename].id > 0).select(): + sessions.append(SessionDb(row)) + return sessions + + +class SessionSetFiles(SessionSet): + """Class representing a set of sessions stored in flat files""" + + def __init__(self, expiration, force, verbose): + SessionSet.__init__(self, expiration, force, verbose) + + def get(self): + """Return list of SessionFile instances for existing sessions.""" + path = os.path.join(request.folder, 'sessions') + return [SessionFile(os.path.join(path, x)) for x in os.listdir(path)] + + +class SessionDb(object): + """Class representing a single session stored in database""" + + def __init__(self, row): + self.row = row + + def delete(self): + self.row.delete_record() + db.commit() + + def get(self): + session = Storage() + session.update(cPickle.loads(self.row.session_data)) + return session + + def last_visit_default(self): + return self.row.modified_datetime + + def __str__(self): + return self.row.unique_key + + +class SessionFile(object): + """Class representing a single session stored as a flat file""" + + def __init__(self, filename): + self.filename = filename + + def delete(self): + os.unlink(self.filename) + + def get(self): + session = Storage() + with open(self.filename, 'rb+') as f: + session.update(cPickle.load(f)) + return session + + def last_visit_default(self): + return datetime.datetime.fromtimestamp( + os.stat(self.filename)[stat.ST_MTIME]) + + def __str__(self): + return self.filename + + +def total_seconds(delta): + """ + Adapted from Python 2.7's timedelta.total_seconds() method. + + Args: + delta: datetime.timedelta instance. + """ + return (delta.microseconds + (delta.seconds + (delta.days * 24 * 3600)) * \ + 10 ** 6) / 10 ** 6 + + +def main(): + """Main processing.""" + + usage = '%prog [options]' + '\nVersion: %s' % VERSION + parser = OptionParser(usage=usage) + + parser.add_option('-f', '--force', + action='store_true', dest='force', default=False, + help=('Ignore session expiration. ' + 'Force expiry based on -x option or auth.settings.expiration.') + ) + parser.add_option('-o', '--once', + action='store_true', dest='once', default=False, + help='Delete sessions, then exit.', + ) + parser.add_option('-s', '--sleep', + dest='sleep', default=SLEEP_MINUTES * 60, type="int", + help='Number of seconds to sleep between executions. Default 300.', + ) + parser.add_option('-v', '--verbose', + default=0, action='count', + help="print verbose output, a second -v increases verbosity") + parser.add_option('-x', '--expiration', + dest='expiration', default=None, type="int", + help='Expiration value for sessions without expiration (in seconds)', + ) + + (options, unused_args) = parser.parse_args() + + expiration = options.expiration + if expiration is None: + try: + expiration = auth.settings.expiration + except: + expiration = EXPIRATION_MINUTES * 60 + + set_db = SessionSetDb(expiration, options.force, options.verbose) + set_files = SessionSetFiles(expiration, options.force, options.verbose) + while True: + set_db.trash() + set_files.trash() + + if options.once: + break + else: + if options.verbose: + print 'Sleeping %s seconds' % (options.sleep) + time.sleep(options.sleep) + + +main() ADDED scripts/standalone_exe_cxfreeze.py Index: scripts/standalone_exe_cxfreeze.py ================================================================== --- scripts/standalone_exe_cxfreeze.py +++ scripts/standalone_exe_cxfreeze.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Usage: + Install cx_Freeze: http://cx-freeze.sourceforge.net/ + Copy script to the web2py directory + c:\Python27\python standalone_exe_cxfreeze.py build_exe +""" +from cx_Freeze import setup, Executable +from gluon.import_all import base_modules, contributed_modules +from gluon.fileutils import readlines_file +from glob import glob +import fnmatch +import os +import shutil +import sys +import re + +#read web2py version from VERSION file +web2py_version_line = readlines_file('VERSION')[0] +#use regular expression to get just the version number +v_re = re.compile('[0-9]+\.[0-9]+\.[0-9]+') +web2py_version = v_re.search(web2py_version_line).group(0) + +base = None + +if sys.platform == 'win32': + base = "Win32GUI" + +base_modules.remove('macpath') +buildOptions = dict( + compressed = True, + excludes = ["macpath","PyQt4"], + includes = base_modules, + include_files=[ + 'applications', + 'ABOUT', + 'LICENSE', + 'VERSION', + 'logging.example.conf', + 'options_std.py', + 'app.example.yaml', + 'queue.example.yaml', + ], + # append any extra module by extending the list below - + # "contributed_modules+["lxml"]" + packages = contributed_modules, + ) + +setup( + name = "Web2py", + version=web2py_version, + author="Massimo DiPierro", + description="web2py web framework", + license = "LGPL v3", + options = dict(build_exe = buildOptions), + executables = [Executable("web2py.py", + base=base, + compress = True, + icon = "web2py.ico", + targetName="web2py.exe", + copyDependentFiles = True)], + ) ADDED scripts/update_web2py.py Index: scripts/update_web2py.py ================================================================== --- scripts/update_web2py.py +++ scripts/update_web2py.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +crontab -e +* 3 * * * root path/to/this/file +""" + +USER = 'www-data' +TMPFILENAME = 'web2py_src_update.zip' + +import sys +import os +import urllib +import zipfile + +if len(sys.argv)>1 and sys.argv[1] == 'nightly': + version = 'http://web2py.com/examples/static/nightly/web2py_src.zip' +else: + version = 'http://web2py.com/examples/static/web2py_src.zip' + +realpath = os.path.realpath(__file__) +path = os.path.dirname(os.path.dirname(os.path.dirname(realpath))) +os.chdir(path) +try: + old_version = open('web2py/VERSION','r').read().strip() +except IOError: + old_version = '' +open(TMPFILENAME,'wb').write(urllib.urlopen(version).read()) +new_version = zipfile.ZipFile(TMPFILENAME).read('web2py/VERSION').strip() +if new_version>old_version: + os.system('sudo -u %s unzip -o %s' % (USER,TMPFILENAME)) + os.system('apachectl restart | apache2ctl restart') Index: scripts/web2py.fedora.sh ================================================================== --- scripts/web2py.fedora.sh +++ scripts/web2py.fedora.sh @@ -38,18 +38,11 @@ return $RETVAL } stop() { echo -n $"Shutting down $DESC ($NAME): " - if [ -r "$PIDFILE" ]; then - pid=`cat $PIDFILE` - kill -TERM $pid - RETVAL=$? - else - RETVAL=1 - fi - [ $RETVAL -eq 0 ] && success || failure + killproc -p "$PIDFILE" -d 3 "$NAME" echo if [ $RETVAL -eq 0 ]; then rm -f /var/lock/subsys/$NAME rm -f $PIDFILE fi Index: setup.py ================================================================== --- setup.py +++ setup.py @@ -62,19 +62,19 @@ 'gluon/contrib/pysimplesoap', 'gluon/contrib/simplejson', 'gluon/tests', ], package_data = {'gluon':['env.tar']}, - scripts = ['mkweb2pyenv','runweb2py'], + scripts = ['w2p_apps','w2p_run','w2p_clone'], ) if __name__ == '__main__': - print "web2py does not require installation and" - print "you should just start it with:" - print - print "$ python web2py.py" - print - print "are you sure you want to install it anyway (y/n)?" - s = raw_input('>') - if s.lower()[:1]=='y': - start() + #print "web2py does not require installation and" + #print "you should just start it with:" + #print + #print "$ python web2py.py" + #print + #print "are you sure you want to install it anyway (y/n)?" + #s = raw_input('>') + #if s.lower()[:1]=='y': + start() Index: setup_app.py ================================================================== --- setup_app.py +++ setup_app.py @@ -50,5 +50,6 @@ 'argv_emulation': True, 'includes': base_modules, 'packages': contributed_modules, }}, setup_requires=['py2app']) + Index: setup_exe.py ================================================================== --- setup_exe.py +++ setup_exe.py @@ -180,5 +180,6 @@ if not make_zip and not remove_build_files: print "Your Windows binary & associated files can also be found in /dist" print "Finished!" print "Enjoy web2py " +web2py_version_line + Index: setup_exe_2.6.py ================================================================== --- setup_exe_2.6.py +++ setup_exe_2.6.py @@ -4,11 +4,11 @@ """ Usage: Install py2exe: http://sourceforge.net/projects/py2exe/files/ Copy script to the web2py directory c:\bin\python26\python build_windows_exe.py py2exe - + Adapted from http://bazaar.launchpad.net/~flavour/sahana-eden/trunk/view/head:/static/scripts/tools/standalone_exe.py """ from distutils.core import setup import py2exe @@ -37,11 +37,11 @@ # Python26 compatibility: http://www.py2exe.org/index.cgi/Tutorial#Step52 try: shutil.copytree('C:\Bin\Microsoft.VC90.CRT', 'dist/') except: print "You MUST copy Microsoft.VC90.CRT folder into the dist directory" - + #read web2py version from VERSION file web2py_version_line = readlines_file('VERSION')[0] #use regular expression to get just the version number v_re = re.compile('[0-9]+\.[0-9]+\.[0-9]+') web2py_version = v_re.search(web2py_version_line).group(0) @@ -81,11 +81,11 @@ print "Deleted Microsoft files not licensed for open source distribution" print "You are still responsible for making sure you have the rights to distribute any other included files!" #delete the API-MS-Win-Core DLLs for f in glob ('dist/API-MS-Win-*.dll'): os.unlink (f) - #then delete some other files belonging to Microsoft + #then delete some other files belonging to Microsoft other_ms_files = ['KERNELBASE.dll', 'MPR.dll', 'MSWSOCK.dll', 'POWRPROF.dll'] for f in other_ms_files: try: os.unlink(os.path.join('dist',f)) except: @@ -111,19 +111,19 @@ if copy_apps.lower().startswith("y"): #copy site-packages if os.path.exists('dist/site-packages') shutil.rmtree('dist/site-packages') shutil.copytree('site-packages', 'dist/site-packages') - #copy scripts + #copy scripts if os.path.exists('dist/scripts'): shutil.rmtree('dist/scripts') shutil.copytree('scripts', 'dist/scripts') else: #no worries, web2py will create the (empty) folder first run print "Skipping site-packages & scripts" pass - + print "" #borrowed from http://bytes.com/topic/python/answers/851018-how-zip-directory-python-using-zipfile def recursive_zip(zipf, directory, folder = ""): @@ -144,11 +144,11 @@ recursive_zip(zipf, path) #leave the first folder as None, as path is root. zipf.close() shutil.rmtree('zip_temp') print "Your Windows binary version of web2py can be found in web2py_win.zip" print "You may extract the archive anywhere and then run web2py/web2py.exe" - + # offer to clear up print "Since you created a zip file you likely do not need the build, deposit and dist folders used while building binary." clean_up_files = raw_input("Delete these un-necessary folders/files? (Y/n) ") if clean_up_files.lower().startswith("y"): shutil.rmtree('build') @@ -158,9 +158,10 @@ print "Your Windows binary & associated files can also be found in /dist" else: #Didn't want zip file created print "" print "Creation of web2py Windows binary completed." - print "You should copy the /dist directory and its contents." + print "You should copy the /dist directory and its contents." print "To run use web2py.exe" print "Finished!" print "Enjoy web2py " +web2py_version_line + Index: web2py.py ================================================================== --- web2py.py +++ web2py.py @@ -14,6 +14,8 @@ # import gluon.import_all ##### This should be uncommented for py2exe.py import gluon.widget # Start Web2py and Web2py cron service! -gluon.widget.start(cron=True) +if __name__ == '__main__': + gluon.widget.start(cron=True) + DELETED web2py_src.zip Index: web2py_src.zip ================================================================== --- web2py_src.zip +++ web2py_src.zip cannot compute difference between binary files Index: wsgihandler.py ================================================================== --- wsgihandler.py +++ wsgihandler.py @@ -40,5 +40,6 @@ application = gluon.main.wsgibase if SOFTCRON: from gluon.settings import global_settings global_settings.web2py_crontype = 'soft' +