├── setup.cfg ├── whirlwind ├── bin │ ├── __init__.py │ └── whirlwind-admin.py ├── conf │ ├── __init__.py │ └── app_template │ │ ├── __init__.py │ │ ├── lib │ │ └── __init__.py │ │ ├── config │ │ ├── __init__.py │ │ ├── routes.py │ │ ├── example.settings.py │ │ ├── options_setup.py │ │ └── settings.py │ │ ├── application │ │ ├── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── user.py │ │ ├── connections │ │ │ ├── __init__.py │ │ │ └── chat.py │ │ ├── controllers │ │ │ ├── __init__.py │ │ │ ├── site_controller.py │ │ │ └── account_controller.py │ │ └── views │ │ │ ├── shared │ │ │ ├── footer.html │ │ │ ├── header.html │ │ │ └── flash_messages.html │ │ │ ├── site │ │ │ └── index.html │ │ │ ├── layouts │ │ │ ├── content.html │ │ │ └── base.html │ │ │ └── account │ │ │ ├── login.html │ │ │ └── signup.html │ │ ├── static │ │ ├── js │ │ │ ├── application.js │ │ │ ├── flash-messages.js │ │ │ ├── contrib │ │ │ │ └── sockjs │ │ │ │ │ └── multiplex.js │ │ │ └── whirlwind.js │ │ ├── images │ │ │ └── readme.txt │ │ ├── errors │ │ │ ├── 500.html │ │ │ ├── 503.html │ │ │ ├── 403.html │ │ │ └── 404.html │ │ └── css │ │ │ ├── common.css │ │ │ ├── layout.css │ │ │ └── reset.css │ │ ├── version.py │ │ └── main.py ├── db │ ├── __init__.py │ ├── redis_interface.py │ ├── memcache_interface.py │ └── mongo.py ├── view │ ├── __init__.py │ ├── paginator.py │ ├── decorators.py │ └── filters.py ├── contrib │ ├── __init__.py │ └── sockjs │ │ ├── __init__.py │ │ ├── sockjs_base_request.py │ │ ├── router_connection.py │ │ ├── multiplex.py │ │ └── bootstrap.py ├── __init__.py ├── core │ ├── __init__.py │ ├── routes.py │ ├── log.py │ ├── bootstrap.py │ ├── multithreading.py │ └── request.py ├── util │ ├── singleton.py │ └── __init__.py └── middleware │ ├── test.py │ ├── session │ ├── middleware.py │ ├── memcache │ │ ├── middleware.py │ │ └── __init__.py │ ├── redis │ │ ├── middleware.py │ │ └── __init__.py │ └── __init__.py │ ├── flash │ ├── middleware.py │ └── __init__.py │ └── __init__.py ├── etc ├── whirlwind.conf └── init.d │ └── whirlwind ├── .gitignore ├── AUTHORS ├── LICENSE ├── INSTALL ├── README.markdown └── setup.py /setup.cfg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/bin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/view/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/contrib/sockjs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/application/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/static/js/application.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/application/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/application/connections/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/application/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /whirlwind/__init__.py: -------------------------------------------------------------------------------- 1 | def get_version(): 2 | return "0.7 Beta" 3 | -------------------------------------------------------------------------------- /etc/whirlwind.conf: -------------------------------------------------------------------------------- 1 | DAEMON_DIR=/path/to/your/whirlwind/app 2 | PORTS=(8000 8001 8002 8003) -------------------------------------------------------------------------------- /whirlwind/conf/app_template/version.py: -------------------------------------------------------------------------------- 1 | #set this to bump the version number output by the version template filter 2 | version = 0 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IGNORED FILES 2 | # 3 | tmp/ 4 | build/* 5 | *.pyc 6 | *.project 7 | *.pydevproject 8 | *.settings 9 | *.sublime* 10 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/static/images/readme.txt: -------------------------------------------------------------------------------- 1 | this is a dumby file so whirlwind-admin.py copies over the folder when creating an app. feel free to remove it. -------------------------------------------------------------------------------- /whirlwind/conf/app_template/static/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |21 | sudo easy_install tornado 22 | sudo easy_install Mako 23 | sudo easy_install MongoKit 24 | sudo easy_install python-dateutil 25 |26 | 27 | ### clone whirlwind 28 |
29 | git clone git://github.com/trendrr/whirlwind.git 30 |31 | 32 | ### change to whirlwind directory 33 |
34 | cd whirlwind 35 |36 | 37 | ### run the setup script 38 |
39 | sudo python setup.py install 40 |41 | 42 | 43 | ## On Windows 44 | 45 | For instructions on how to install **whirlwind** on windows, visit [this page](https://github.com/trendrr/whirlwind/wiki/Windowsinstall). 46 | 47 | ### create a whirlwind app 48 |
49 | whirlwind-admin.py --create-application myapp 50 |51 | 52 | ### fill in env specific settings in settings file [config/settings.py] 53 | 54 | Then `cd` into `myapp` (the name of the app you created) 55 |
56 | # generate a cookie secret 57 | whirlwind-admin.py --gcs 58 |59 | 60 | An auto-generated secret should be printed to standard output. 61 | 62 | ### set the variable `cookie_secret` in config/settings.py to the printed out secret 63 |
64 | cookie_secret = "setthistoyourowncookiesecret" 65 |66 | 67 | ### then start your app 68 |
69 | python main.py 70 |71 | 72 | ### that's it! now just point your browser to the app start page to see whirlwind in action. 73 |
74 | http://localhost:8000/ 75 |76 | 77 | If you encounter any problems please refer to our [FAQ](http://github.com/trendrr/whirlwind/wiki/FAQ) for troubleshooting instructions or post a new issue. 78 | 79 | Copyright © 2010-2012 Trendrr 80 | 81 | Permission is hereby granted, free of charge, to any person obtaining a copy 82 | of this software and associated documentation files (the "Software"), to deal 83 | in the Software without restriction, including without limitation the rights 84 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 85 | copies of the Software, and to permit persons to whom the Software is 86 | furnished to do so, subject to the following conditions: 87 | 88 | The above copyright notice and this permission notice shall be included in 89 | all copies or substantial portions of the Software. 90 | 91 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 92 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 93 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 94 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 95 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 96 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 97 | THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from distutils.command.install_data import install_data 3 | from distutils.command.install import INSTALL_SCHEMES 4 | import os 5 | import sys 6 | 7 | class osx_install_data(install_data): 8 | # On MacOS, the platform-specific lib dir is /System/Library/Framework/Python/.../ 9 | # which is wrong. Python 2.5 supplied with MacOS 10.5 has an Apple-specific fix 10 | # for this in distutils.command.install_data#306. It fixes install_lib but not 11 | # install_data, which is why we roll our own install_data class. 12 | 13 | def finalize_options(self): 14 | # By the time finalize_options is called, install.install_lib is set to the 15 | # fixed directory, so we set the installdir to install_lib. The 16 | # install_data class uses ('install_data', 'install_dir') instead. 17 | self.set_undefined_options('install', ('install_lib', 'install_dir')) 18 | install_data.finalize_options(self) 19 | 20 | if sys.platform == "darwin": 21 | cmdclasses = {'install_data': osx_install_data} 22 | else: 23 | cmdclasses = {'install_data': install_data} 24 | 25 | def fullsplit(path, result=None): 26 | """ 27 | Split a pathname into components (the opposite of os.path.join) in a 28 | platform-neutral way. 29 | """ 30 | if result is None: 31 | result = [] 32 | head, tail = os.path.split(path) 33 | if head == '': 34 | return [tail] + result 35 | if head == path: 36 | return result 37 | return fullsplit(head, [tail] + result) 38 | 39 | # Tell distutils to put the data_files in platform-specific installation 40 | # locations. See here for an explanation: 41 | # http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb 42 | for scheme in INSTALL_SCHEMES.values(): 43 | scheme['data'] = scheme['purelib'] 44 | 45 | # Compile the list of packages available, because distutils doesn't have 46 | # an easy way to do this. 47 | packages, data_files = [], [] 48 | root_dir = os.path.dirname(__file__) 49 | if root_dir != '': 50 | os.chdir(root_dir) 51 | whirlwind_dir = 'whirlwind' 52 | 53 | for dirpath, dirnames, filenames in os.walk(whirlwind_dir): 54 | # Ignore dirnames that start with '.' 55 | for i, dirname in enumerate(dirnames): 56 | if dirname.startswith('.'): del dirnames[i] 57 | if '__init__.py' in filenames: 58 | packages.append('.'.join(fullsplit(dirpath))) 59 | elif filenames: 60 | data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) 61 | 62 | # Small hack for working with bdist_wininst. 63 | # See http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html 64 | if len(sys.argv) > 1 and sys.argv[1] == 'bdist_wininst': 65 | for file_info in data_files: 66 | file_info[0] = '\\PURELIB\\%s' % file_info[0] 67 | 68 | 69 | version = __import__('whirlwind').get_version() 70 | 71 | setup( 72 | name = "WhirlWind", 73 | version = version.replace(' ', '-'), 74 | url = 'https://github.com/trendrr/whirlwind', 75 | author = 'Trendrr', 76 | author_email = 'admin@trendrr.com', 77 | description = 'A highly scaleable Python web framework.', 78 | download_url = 'https://github.com/trendrr/whirlwind/tarball/master', 79 | packages = packages, 80 | cmdclass = cmdclasses, 81 | data_files = data_files, 82 | scripts = ['whirlwind/bin/whirlwind-admin.py'], 83 | classifiers = ['Development Status :: 4 - Beta', 84 | 'Environment :: Web Environment', 85 | 'Framework :: WhirlWind', 86 | 'Intended Audience :: Developers', 87 | 'License :: OSI Approved :: MIT License', 88 | 'Operating System :: OS Independent', 89 | 'Programming Language :: Python', 90 | 'Topic :: Internet :: WWW/HTTP', 91 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 92 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 93 | 'Topic :: Software Development :: Libraries :: Python Modules', 94 | ], 95 | ) -------------------------------------------------------------------------------- /whirlwind/middleware/session/memcache/__init__.py: -------------------------------------------------------------------------------- 1 | import pymongo, uuid, datetime 2 | from datetime import timedelta 3 | from whirlwind.db.mongo import Mongo 4 | from whirlwind.db.memcache_interface import Memcache 5 | from application.models.user import User 6 | import memcache 7 | 8 | try: 9 | import simplejson as json 10 | from config.settings import cookie_domain 11 | except ImportError: 12 | import json 13 | 14 | ''' 15 | Redis Session backend for Tornado 16 | 17 | 18 | sudo easy_install python-memcache 19 | 20 | ''' 21 | class Session(): 22 | 23 | ''' 24 | constructor... load or create the session 25 | ''' 26 | def __init__(self,request_handler): 27 | 28 | self.saved = False 29 | 30 | #look for a sessionId 31 | # we allow a sessionId param, as this is necessary to get authenticated ajax requests from the 32 | # api. 33 | self.session_id = request_handler.get_argument('xdt', request_handler.get_secure_cookie("sessionId")) 34 | 35 | 36 | if not self.session_id or not self.__lookup_session(request_handler) : 37 | self.__create() 38 | if 'cookie_domain' in locals(): 39 | request_handler.set_secure_cookie("sessionId", self.session_id, domain=cookie_domain) 40 | else: 41 | request_handler.set_secure_cookie("sessionId", self.session_id) 42 | # print self.data 43 | 44 | ''' 45 | destructor... updates the session db in redis if our session data has been changed 46 | ''' 47 | def __del__(self): 48 | # print "session destruction" 49 | self.save() 50 | 51 | 52 | ''' 53 | finalizes and saves the session. if not called explicitly then 54 | this will be called in the destructor. 55 | ''' 56 | def save(self): 57 | if self.saved : 58 | # print 'already saved. skipping' 59 | return 60 | 61 | updateHours = 8 62 | 63 | if self['keep_logged_in'] : 64 | updateHours = 24*7 #if you checked keep me logged in then we roll by a week 65 | 66 | expire_seconds = updateHours * 60 * 60 67 | # Save the data as a json string 68 | Memcache.db.set(self.session_id, json.dumps(self.data),expire_seconds); 69 | 70 | self.saved = True 71 | 72 | 73 | ''' 74 | get a value from our session 75 | ''' 76 | def __getitem__(self, key): 77 | if key == '_id' : 78 | return self.session_id 79 | 80 | try: 81 | return self.data[key] 82 | except KeyError: 83 | self.data.update({key:None}) 84 | return None 85 | 86 | ''' 87 | set a value in our session 88 | ''' 89 | def __setitem__(self, key, value): 90 | if not self.session_id: 91 | #no session yet so lets create a session 92 | self.__create() 93 | self.data[key] = value 94 | self.saved = False 95 | 96 | ''' 97 | delete a value from our session 98 | ''' 99 | def __delitem__(self, key): 100 | del self.data[key] 101 | self.saved = False 102 | 103 | ''' 104 | get a session value or return an optional default value 105 | ''' 106 | def get(self,key,default=None): 107 | #grab a value from the session object 108 | val = self.__getitem__(key) 109 | #if the val isn't there return the default 110 | if val == None: 111 | return default 112 | else: 113 | return val 114 | 115 | ''' 116 | private helper functions 117 | ''' 118 | def __create(self): 119 | #generate a session id 120 | self.__generate_session_id() 121 | #insert the new doc into mongo 122 | print "creating new session" 123 | #store the session_id in a secure cookie 124 | self.data = {} 125 | 126 | ''' 127 | lookup a session from the db based on the session id 128 | 129 | returns true on successful lookup, false if expired. 130 | ''' 131 | def __lookup_session(self, request_handler): 132 | val = Memcache.db.get(self.session_id) 133 | if not val : 134 | return False 135 | 136 | self.data = json.loads(val) 137 | #load the user 138 | if self['username']: 139 | user = Mongo.db.ui.users.User.find_one({'_id': self['username']}) 140 | request_handler.set_current_user(user) 141 | 142 | return True 143 | 144 | ''' 145 | generate a uniqe id for our session 146 | ''' 147 | def __generate_session_id(self): 148 | self.session_id = str(uuid.uuid4()) 149 | 150 | 151 | ''' 152 | destroys the current session. deletes from the database. 153 | ''' 154 | def destroy(self): 155 | Memcache.db.delete(self.session_id) 156 | self.saved = True -------------------------------------------------------------------------------- /whirlwind/middleware/session/redis/__init__.py: -------------------------------------------------------------------------------- 1 | import pymongo, uuid, datetime 2 | from datetime import timedelta 3 | from whirlwind.db.mongo import Mongo 4 | from whirlwind.db.redis_interface import Redis 5 | from application.models.user import User 6 | import redis 7 | 8 | try: 9 | import simplejson as json 10 | from config.settings import cookie_domain 11 | except ImportError: 12 | import json 13 | 14 | ''' 15 | Redis Session backend for Tornado 16 | 17 | 18 | sudo easy_install redis 19 | 20 | ''' 21 | class Session(): 22 | 23 | ''' 24 | constructor... load or create the session 25 | ''' 26 | def __init__(self,request_handler): 27 | 28 | self.saved = False 29 | 30 | #look for a sessionId 31 | # we allow a sessionId param, as this is necessary to get authenticated ajax requests from the 32 | # api. 33 | self.session_id = request_handler.get_argument('xdt', request_handler.get_secure_cookie("sessionId")) 34 | 35 | 36 | if not self.session_id or not self.__lookup_session(request_handler) : 37 | self.__create() 38 | if 'cookie_domain' in locals(): 39 | request_handler.set_secure_cookie("sessionId", self.session_id, domain=cookie_domain) 40 | else: 41 | request_handler.set_secure_cookie("sessionId", self.session_id) 42 | # print self.data 43 | 44 | ''' 45 | destructor... updates the session db in redis if our session data has been changed 46 | ''' 47 | def __del__(self): 48 | # print "session destruction" 49 | self.save() 50 | 51 | 52 | ''' 53 | finalizes and saves the session. if not called explicitly then 54 | this will be called in the destructor. 55 | ''' 56 | def save(self): 57 | if self.saved : 58 | # print 'already saved. skipping' 59 | return 60 | 61 | updateHours = 8 62 | 63 | if self['keep_logged_in'] : 64 | updateHours = 24*7 #if you checked keep me logged in then we roll by a week 65 | 66 | expires = datetime.datetime.utcnow() + timedelta(hours=updateHours) 67 | # Save the data as a json string 68 | Redis.db.set(self.session_id, json.dumps(self.data)); 69 | # Update the ttl 70 | Redis.db.expireat(self.session_id, expires) 71 | self.saved = True 72 | 73 | 74 | ''' 75 | get a value from our session 76 | ''' 77 | def __getitem__(self, key): 78 | if key == '_id' : 79 | return self.session_id 80 | 81 | try: 82 | return self.data[key] 83 | except KeyError: 84 | self.data.update({key:None}) 85 | return None 86 | 87 | ''' 88 | set a value in our session 89 | ''' 90 | def __setitem__(self, key, value): 91 | if not self.session_id: 92 | #no session yet so lets create a session 93 | self.__create() 94 | self.data[key] = value 95 | self.saved = False 96 | 97 | ''' 98 | delete a value from our session 99 | ''' 100 | def __delitem__(self, key): 101 | del self.data[key] 102 | self.saved = False 103 | 104 | ''' 105 | get a session value or return an optional default value 106 | ''' 107 | def get(self,key,default=None): 108 | #grab a value from the session object 109 | val = self.__getitem__(key) 110 | #if the val isn't there return the default 111 | if val == None: 112 | return default 113 | else: 114 | return val 115 | 116 | ''' 117 | private helper functions 118 | ''' 119 | def __create(self): 120 | #generate a session id 121 | self.__generate_session_id() 122 | #insert the new doc into mongo 123 | print "creating new session" 124 | #store the session_id in a secure cookie 125 | self.data = {} 126 | 127 | ''' 128 | lookup a session from the db based on the session id 129 | 130 | returns true on successful lookup, false if expired. 131 | ''' 132 | def __lookup_session(self, request_handler): 133 | val = Redis.db.get(self.session_id) 134 | if not val : 135 | return False 136 | 137 | self.data = json.loads(val) 138 | #load the user 139 | if self['username']: 140 | user = Mongo.db.ui.users.User.find_one({'_id': self['username']}) 141 | request_handler.set_current_user(user) 142 | 143 | return True 144 | 145 | ''' 146 | generate a uniqe id for our session 147 | ''' 148 | def __generate_session_id(self): 149 | self.session_id = str(uuid.uuid4()) 150 | 151 | 152 | ''' 153 | destroys the current session. deletes from the database. 154 | ''' 155 | def destroy(self): 156 | Redis.db.delete(self.session_id) 157 | self.saved = True -------------------------------------------------------------------------------- /whirlwind/bin/whirlwind-admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import whirlwind 4 | import os 5 | import logging 6 | import optparse 7 | from distutils import dir_util 8 | 9 | ''' 10 | whirlwind-admin.py --version 11 | 12 | whirlwind-admin.py --create-application app_name 13 | 14 | whirlwind-admin.py --generate-cookie-secret 15 | 16 | whirlwind-admin.py --generate-model-indexes 17 | ''' 18 | 19 | logging.basicConfig(level=logging.INFO, 20 | format='%(asctime)s %(levelname)s %(message)s') 21 | 22 | 23 | def main(): 24 | 25 | usage = "usage: %prog [options [args]]" 26 | parser = optparse.OptionParser(usage) 27 | 28 | parser.add_option("--ca", "--create-application", 29 | dest="create_app", 30 | metavar="FILE", 31 | action="store_true", 32 | default=False, 33 | help="Creates an application structure") 34 | 35 | parser.add_option("--v", "--version", dest="version", 36 | action="store_true", 37 | default=False, 38 | help="Print the version info for this release of WhirlWind") 39 | 40 | parser.add_option("--gcs", "--generate-cookie-secret", dest="generate_cookie_secret", 41 | action="store_true", 42 | default=False, 43 | help="Generate a cookie secret hash") 44 | 45 | parser.add_option("--gmi", "--generate-model-indexes", dest="generate_model_indexes", 46 | action="store_true", 47 | default=False, 48 | help="Generate mongo indexes for your models") 49 | 50 | options, args = parser.parse_args() 51 | 52 | if not options.create_app and not options.version and not options.generate_cookie_secret and not options.generate_model_indexes: 53 | parser.error('Must choose one -- try --ca or --v or --gcs or --gmi') 54 | 55 | if options.create_app: 56 | 57 | if len(args) != 1: 58 | logging.error("Error no app name given") 59 | return 60 | 61 | #generate the template dir path 62 | template_dir = os.path.join(whirlwind.__path__[0], 'conf', 'app_template') 63 | 64 | #copy the template files 65 | copied_files = dir_util.copy_tree(template_dir, args[0]) 66 | 67 | #check that we copied files 68 | if len(copied_files) > 0: 69 | logging.info('Created %s' % options.create_app) 70 | else: 71 | logging.info('Error copying app template') 72 | 73 | if options.version: 74 | logging.info(whirlwind.get_version()) 75 | 76 | if options.generate_cookie_secret: 77 | import base64 78 | import uuid 79 | print(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)) 80 | 81 | if options.generate_model_indexes: 82 | 83 | import sys 84 | import pkgutil 85 | from whirlwind.db.mongo import Mongo 86 | 87 | #insert the current dir into the path 88 | sys.path.insert(0, os.getcwd()) 89 | 90 | #grab a settings path from our args if exists or assume relative default 91 | settings_module = args[0] if len(args) == 1 else 'config.settings' 92 | 93 | conf = __import__(settings_module) 94 | 95 | #connect to our db using our options set in settings.py 96 | Mongo.create(host=conf.settings.db_host, port=conf.settings.db_port) 97 | 98 | #import our default models package 99 | __import__('application.models') 100 | 101 | pkg_mods = sys.modules['application.models'] 102 | 103 | #setup a prefix string 104 | prefix = pkg_mods.__name__ + "." 105 | 106 | #import all the modules in the models dir so the registration decorators fire 107 | for importer, modname, ispkg in pkgutil.iter_modules(pkg_mods.__path__, prefix): 108 | __import__(modname) 109 | 110 | #loop over the registered documents 111 | for doc, obj in Mongo.db.connection._registered_documents.iteritems(): 112 | try: 113 | 114 | print 'Attempting to create index for ', doc 115 | #generate the index for this doc on the collection 116 | obj.generate_index(Mongo.db.connection[conf.settings.db_name][obj._obj_class.__collection__]) 117 | except Exception, e: 118 | #barf up an error on fail 119 | print 'Could not create index for %s - exception: %s' % (doc, e.message) 120 | 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/application/controllers/account_controller.py: -------------------------------------------------------------------------------- 1 | from whirlwind.core.request import BaseRequest 2 | from whirlwind.db.mongo import Mongo 3 | from application.models.user import User 4 | import datetime, hashlib 5 | from tornado.web import authenticated 6 | from whirlwind.view.decorators import route 7 | 8 | @route('/logout') 9 | class LogoutHandler(BaseRequest): 10 | def get(self): 11 | self.session['username'] = None 12 | self.session.destroy() 13 | #kill the session. 14 | self.redirect("/") 15 | 16 | @route('/login') 17 | class LoginHandler(BaseRequest): 18 | def get(self): 19 | template_values = {} 20 | template_values['next'] = self.get_argument('next','/') 21 | 22 | self.render_template('/account/login.html',**template_values) 23 | 24 | def post(self): 25 | username = self.get_argument("username",None) 26 | password = self.get_argument("password",None) 27 | 28 | if not username or not password: 29 | # do something 30 | self.flash.error = "You must enter a username and password to proceed. Please try again." 31 | self.redirect("/login") 32 | return 33 | 34 | pw = hashlib.sha1(password).hexdigest() 35 | username = User.normalize(username) 36 | user = User.lookup(username) 37 | 38 | #check the password. 39 | if not user or user['password'] != pw: 40 | # do something 41 | self.flash.error = "Login not valid" 42 | self.redirect("/login") 43 | return 44 | 45 | # check if user is suspended. 46 | if user.is_suspended() : 47 | self.flash.error = "Sorry the account you specified has been suspended." 48 | self.redirect("/") 49 | return 50 | 51 | 52 | user.history.last_login = datetime.datetime.utcnow() 53 | Mongo.db.ui.users.update({'_id': username}, { 54 | '$set' : {'history.last_login': user.history.last_login}, 55 | '$inc' : {'history.num_logins' : 1} 56 | }) 57 | #add to the session. 58 | self.session['username'] = user._id 59 | #check keep_logged_in 60 | if self.get_argument("keep_logged_in", False) == "on" : 61 | self.session['keep_logged_in'] = True 62 | 63 | self.set_current_user(user) 64 | self.flash.notice = "Welcome, %s" % user._id 65 | forwardUrl = self.get_argument('next','/') 66 | self.redirect(forwardUrl) 67 | 68 | @route('/signup') 69 | class SignupHandler(BaseRequest): 70 | def get(self): 71 | template_values = {} 72 | template_values['next'] = self.get_argument('next','/') 73 | 74 | self.render_template('/account/signup.html',**template_values) 75 | 76 | def post(self): 77 | username = self.get_argument("username",None) 78 | password = self.get_argument("password",None) 79 | 80 | if not username or not password: 81 | # do something 82 | self.flash.error = "You must enter a username and password to proceed. Please try again." 83 | self.redirect("/signup") 84 | return 85 | 86 | if password != self.get_argument("password2", None) : 87 | self.flash.error = "Passwords do not match. Please try again." 88 | self.redirect("/signup") 89 | return 90 | 91 | user = User.instance(username, password) 92 | Mongo.db.ui.users.insert(user) 93 | self.flash.info = "Successfully created your account, please log in." 94 | self.redirect("/login") 95 | 96 | class PasswordChanger(BaseRequest): 97 | @authenticated 98 | def post(self): 99 | pw = hashlib.sha1(self.get_argument("password")).hexdigest() 100 | 101 | if self.get_current_user()['password'] != pw: 102 | # do something 103 | self.flash.error = "Password not valid, please try again" 104 | self.redirect("/settings") 105 | return 106 | 107 | newPw = self.get_argument('new_pw') 108 | newPw2 = self.get_argument('new_pw_again') 109 | if newPw != newPw2 : 110 | self.flash.error = "Passwords do not match, please try again" 111 | self.redirect("/settings") 112 | return 113 | 114 | password = hashlib.sha1(newPw).hexdigest() 115 | Mongo.db.ui.users.update({'_id': self.get_username()}, { 116 | '$set' : {'password': password} 117 | }) 118 | self.flash.success = "Successfully updated password" 119 | self.redirect('/settings') 120 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/static/css/reset.css: -------------------------------------------------------------------------------- 1 | /* HTML5 Boilerplate */ 2 | 3 | article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } 4 | audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } 5 | audio:not([controls]) { display: none; } 6 | [hidden] { display: none; } 7 | 8 | html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } 9 | html, button, input, select, textarea { font-family: sans-serif; color: #222; } 10 | body { margin: 0; font-size: 1em; line-height: 1.4; } 11 | 12 | ::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; } 13 | ::selection { background: #fe57a1; color: #fff; text-shadow: none; } 14 | 15 | a { color: #00e; } 16 | a:visited { color: #551a8b; } 17 | a:hover { color: #06e; } 18 | a:focus { outline: thin dotted; } 19 | a:hover, a:active { outline: 0; } 20 | abbr[title] { border-bottom: 1px dotted; } 21 | b, strong { font-weight: bold; } 22 | blockquote { margin: 1em 40px; } 23 | dfn { font-style: italic; } 24 | hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } 25 | ins { background: #ff9; color: #000; text-decoration: none; } 26 | mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } 27 | pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; } 28 | pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } 29 | 30 | q { quotes: none; } 31 | q:before, q:after { content: ""; content: none; } 32 | small { font-size: 85%; } 33 | sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } 34 | sup { top: -0.5em; } 35 | sub { bottom: -0.25em; } 36 | 37 | ul, ol { margin: 1em 0; padding: 0 0 0 40px; } 38 | dd { margin: 0 0 0 40px; } 39 | nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; } 40 | 41 | img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } 42 | svg:not(:root) { overflow: hidden; } 43 | figure { margin: 0; } 44 | 45 | form { margin: 0; } 46 | fieldset { border: 0; margin: 0; padding: 0; } 47 | 48 | label { cursor: pointer; } 49 | legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal; } 50 | button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; } 51 | button, input { line-height: normal; } 52 | button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; } 53 | button[disabled], input[disabled] { cursor: default; } 54 | input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px; } 55 | input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; } 56 | input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } 57 | button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } 58 | textarea { overflow: auto; vertical-align: top; resize: vertical; } 59 | input:valid, textarea:valid { } 60 | input:invalid, textarea:invalid { background-color: #f0dddd; } 61 | 62 | table { border-collapse: collapse; border-spacing: 0; } 63 | td { vertical-align: top; } 64 | 65 | .chromeframe { margin: 0.2em 0; background: #ccc; color: black; padding: 0.2em 0; } 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | @media only screen and (min-width: 35em) { 76 | 77 | 78 | } 79 | 80 | .ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0; } 81 | .ir br { display: none; } 82 | .hidden { display: none !important; visibility: hidden; } 83 | .visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } 84 | .visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } 85 | .invisible { visibility: hidden; } 86 | .clearfix:before, .clearfix:after { content: ""; display: table; } 87 | .clearfix:after { clear: both; } 88 | .clearfix { *zoom: 1; } 89 | 90 | @media print { 91 | * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } 92 | a, a:visited { text-decoration: underline; } 93 | a[href]:after { content: " (" attr(href) ")"; } 94 | abbr[title]:after { content: " (" attr(title) ")"; } 95 | .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } 96 | pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } 97 | thead { display: table-header-group; } 98 | tr, img { page-break-inside: avoid; } 99 | img { max-width: 100% !important; } 100 | @page { margin: 0.5cm; } 101 | p, h2, h3 { orphans: 3; widows: 3; } 102 | h2, h3 { page-break-after: avoid; } 103 | } 104 | -------------------------------------------------------------------------------- /whirlwind/middleware/session/__init__.py: -------------------------------------------------------------------------------- 1 | import pymongo, uuid, datetime 2 | from datetime import timedelta 3 | from whirlwind.db.mongo import Mongo 4 | 5 | try: 6 | from config.settings import cookie_domain 7 | except ImportError: 8 | pass 9 | ''' 10 | MongoDb Session backend for Tornado 11 | ''' 12 | class Session(): 13 | 14 | ''' 15 | constructor... load or create the session 16 | ''' 17 | def __init__(self,request_handler): 18 | #create a data changed flag 19 | self.data_changed = False 20 | self.saved = False 21 | 22 | #look for a sessionId 23 | # we allow a sessionId param, as this is necessary to get authenticated ajax requests from the 24 | # api. 25 | self.session_id = request_handler.get_argument('xdt', request_handler.get_secure_cookie("sessionId")) 26 | 27 | 28 | if not self.session_id or not self.__lookup_session(request_handler) : 29 | self.__create() 30 | if 'cookie_domain' in locals(): 31 | request_handler.set_secure_cookie("sessionId", self.session_id, domain=cookie_domain) 32 | else: 33 | request_handler.set_secure_cookie("sessionId", self.session_id) 34 | # print self.data 35 | 36 | ''' 37 | destructor... updates the session db in mongo if our session data has been changed 38 | ''' 39 | def __del__(self): 40 | # print "session destruction" 41 | self.save() 42 | 43 | 44 | ''' 45 | finalizes and saves the session. if not called explicitly then 46 | this will be called in the destructor. 47 | ''' 48 | def save(self): 49 | if self.saved : 50 | # print 'already saved. skipping' 51 | return 52 | 53 | updateHours = 8 54 | 55 | if self['keep_logged_in'] : 56 | updateHours = 24*7 #if you checked keep me logged in then we roll by a week 57 | 58 | self.data['expires'] = datetime.datetime.utcnow() + timedelta(hours=updateHours) 59 | 60 | if self.data_changed: 61 | #update the timestamp for rolling session timeout 62 | #update the session data in mongo 63 | Mongo.db.ui.sessions.save(self.data) 64 | else : 65 | # just update the expires field so rolling timeout works. 66 | Mongo.db.ui.sessions.update({'_id':self.session_id},{'$set' : {'expires' : self.data['expires']}}) 67 | # print "SAVING session expires", self.data['expires'] 68 | self.saved = True 69 | self.data_changed = False 70 | 71 | 72 | ''' 73 | get a value from our session 74 | ''' 75 | def __getitem__(self, key): 76 | try: 77 | return self.data[key] 78 | except KeyError: 79 | self.data.update({key:None}) 80 | return None 81 | 82 | ''' 83 | set a value in our session 84 | ''' 85 | def __setitem__(self, key, value): 86 | if not self.session_id: 87 | #no session yet so lets create a session 88 | self.__create() 89 | 90 | self.data[key] = value 91 | self.data_changed = True 92 | self.saved = False 93 | 94 | ''' 95 | delete a value from our session 96 | ''' 97 | def __delitem__(self, key): 98 | del self.data[key] 99 | self.data_changed = True 100 | self.saved = False 101 | 102 | ''' 103 | get a session value or return an optional default value 104 | ''' 105 | def get(self,key,default=None): 106 | #grab a value from the session object 107 | val = self.__getitem__(key) 108 | #if the val isn't there return the default 109 | if val == None: 110 | return default 111 | else: 112 | return val 113 | 114 | ''' 115 | private helper functions 116 | ''' 117 | def __create(self): 118 | #generate a session id 119 | self.__generate_session_id() 120 | #insert the new doc into mongo 121 | print "creating new session" 122 | #store the session_id in a secure cookie 123 | self.data = { 124 | '_id':self.session_id, 125 | 'expires':datetime.datetime.utcnow() + timedelta(hours=8) 126 | } 127 | Mongo.db.ui.sessions.insert(self.data) 128 | 129 | ''' 130 | lookup a session from the db based on the session id 131 | 132 | returns true on successful lookup, false if expired. 133 | ''' 134 | def __lookup_session(self, request_handler): 135 | self.data = Mongo.db.ui.sessions.find_one({'_id': self.session_id}) 136 | 137 | if not self.data: 138 | print "session not found in database" 139 | return False 140 | 141 | #check if this session has expired. 142 | print "session expires:", self.data['expires'] 143 | if not self.data['expires'] or self.data['expires'] < datetime.datetime.utcnow() : 144 | print "SESSION EXPIRED!" 145 | self.destroy() #delete it from the database.. 146 | return False 147 | 148 | #load the user 149 | if self['lookup_key']: 150 | key = self['lookup_key'] 151 | user = Mongo.db.ui.users.User.find_one({key: self[key]}) 152 | request_handler.set_current_user(user) 153 | elif self['username']: 154 | user = Mongo.db.ui.users.User.find_one({'_id': self['username']}) 155 | request_handler.set_current_user(user) 156 | 157 | return True 158 | 159 | ''' 160 | generate a uniqe id for our session 161 | ''' 162 | def __generate_session_id(self): 163 | self.session_id = str(uuid.uuid4()) 164 | 165 | 166 | ''' 167 | destroys the current session. deletes from the database. 168 | ''' 169 | def destroy(self): 170 | Mongo.db.ui.sessions.remove({'_id': self.session_id}) 171 | self.data_changed = False 172 | self.saved = True -------------------------------------------------------------------------------- /whirlwind/view/filters.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import json 4 | import locale 5 | from tornado.options import options 6 | from dateutil import tz 7 | 8 | 9 | class Filters(): 10 | 11 | ''' 12 | Checks whether the passed in value is considered useful otherwise will return None 13 | 14 | will return None on the following values: 15 | None 16 | '' 17 | 'null' 18 | 'undefined' 19 | {} 20 | ''' 21 | @staticmethod 22 | def val(val): 23 | if val == None: 24 | return None 25 | if val == 'null': 26 | return None 27 | if val == 'undefined': 28 | return None 29 | if val == 0: 30 | return val 31 | if isinstance(val, basestring) and len(val) == 0: 32 | return None 33 | if isinstance(val, dict) and len(val) == 0: 34 | return None 35 | return val 36 | 37 | @staticmethod 38 | def version(): 39 | try: 40 | return options.version 41 | except: 42 | return '' 43 | 44 | @staticmethod 45 | def str(val): 46 | if not val: 47 | return '' 48 | #TODO: sensibly handle: 49 | # dicts => json 50 | # dates => pretty 51 | # numbers => add commas 52 | return str(val) 53 | 54 | ''' 55 | Checks for various styles of true. 56 | matches on True, 'true', 'on' 57 | ''' 58 | @staticmethod 59 | def is_true(val): 60 | if not val: 61 | return False 62 | if isinstance(val, basestring): 63 | if val == 'True' or val == 'true' or val == 'on': 64 | return True 65 | return False 66 | if val == True: 67 | return True 68 | return False 69 | 70 | @staticmethod 71 | def strip_html(data): 72 | if not data: 73 | return 74 | p = re.compile(r'<[^<]*?/?>') 75 | return p.sub('', data) 76 | 77 | @staticmethod 78 | def long_timestamp(dt_str, tz="America/New_York"): 79 | utc_dt = Filters._convert_utc_to_local(dt_str, tz) 80 | if utc_dt: 81 | return utc_dt.strftime("%A, %d. %B %Y %I:% %p") 82 | else: 83 | return dt_str 84 | 85 | @staticmethod 86 | def short_timestamp(dt_str, tz="America/New_York"): 87 | tz_dt = Filters._convert_utc_to_local(dt_str, tz) 88 | return tz_dt.strftime("%m/%d/%Y %I:%M %p") 89 | 90 | @staticmethod 91 | def short_date(dt_str, tz="America/New_York"): 92 | tz_dt = Filters._convert_utc_to_local(dt_str, tz) 93 | return tz_dt.strftime("%m/%d/%Y") 94 | 95 | @staticmethod 96 | def ellipsis(data, limit, append='...'): 97 | return (data[:limit] + append) if len(data) > limit else data 98 | 99 | ''' 100 | filter to translate a dict to json 101 | ''' 102 | @staticmethod 103 | def to_json(dict): 104 | return json.dumps(dict, True) 105 | 106 | @staticmethod 107 | def idize(str): 108 | return (re.sub(r'[^0-9a-zA-Z]', '_', str)).lower() 109 | 110 | @staticmethod 111 | def _convert_utc_to_local(utc_dt, timezone): 112 | try: 113 | from_zone = tz.gettz('UTC') 114 | to_zone = tz.gettz(timezone) 115 | utc_dt = utc_dt.replace(tzinfo=from_zone) 116 | return utc_dt.astimezone(to_zone) 117 | except Exception: 118 | print sys.exc_info() 119 | return None 120 | 121 | @staticmethod 122 | def url_pretty(str): 123 | if not str: 124 | return 125 | 126 | url = re.sub(r'[^0-9a-zA-Z]', '_', Filters.str(str)) 127 | url = re.sub('_+', '_', url) 128 | #max 32 chars. 129 | if len(url) > 32: 130 | url = url[0:32] 131 | return url 132 | 133 | @staticmethod 134 | def add_commas(val, as_data_type='int', the_locale=locale.LC_ALL): 135 | locale.setlocale(the_locale, "") 136 | if as_data_type == 'int': 137 | return locale.format('%d', int(val), True) 138 | elif as_data_type == 'float': 139 | return locale.format('%f', float(val), True) 140 | else: 141 | return val 142 | 143 | @staticmethod 144 | def get_time_string(str): 145 | if str == "N/A": 146 | return str 147 | 148 | parts = str.split("/") 149 | isPM = parts[0].find('am') == -1 150 | if not isPM: 151 | parts[0] = parts[0].replace("am", "") 152 | 153 | parts[1] = parts[1].replace("c", "") 154 | if(len(parts[0]) >= 3): 155 | if(len(parts[0]) == 4): 156 | parts[0] = parts[0][0:2] + ":" + parts[0][2:] 157 | else: 158 | parts[0] = parts[0][:1] + ":" + parts[0][1:] 159 | if(len(parts[1]) >= 3): 160 | if(len(parts[1]) == 4): 161 | parts[1] = parts[1][0:2] + ":" + parts[1][2:] 162 | else: 163 | parts[1] = parts[1][:1] + ":" + parts[1][1:] 164 | 165 | if isPM: 166 | time = parts[0] + "/" + parts[1] + "c" 167 | else: 168 | time = parts[0] + "am/" + parts[1] + "c" 169 | 170 | return time 171 | 172 | @staticmethod 173 | def pluralize(str): 174 | pl = Pluralizer() 175 | return pl.plural(str) 176 | 177 | ''' 178 | Does a get on the dict. will work with dot operator, and not throw an exception 179 | returns default if the key doesn't work 180 | 181 | will also work to reach into lists via integer keys. 182 | 183 | example: 184 | { 185 | 'key1' : { 186 | 'subkey' : [{'subsubkey1':9},{}] 187 | } 188 | } 189 | Filters.dict_get('key1.subkey.0.subsubkey1') => 9 190 | ''' 191 | @staticmethod 192 | def dict_get(dict, key, default=None): 193 | #Surround this with try in case key is None or not a string or something 194 | try: 195 | keys = key.split(".") 196 | except: 197 | return default 198 | 199 | tmp = dict 200 | for k in keys: 201 | try: 202 | tmp = tmp[k] 203 | except TypeError: 204 | #Issue may be that we have something like '0'. Try converting to a number 205 | try: 206 | tmp = tmp[int(k)] 207 | except: 208 | #Either couldn't convert or went out of bounds on list 209 | return default 210 | except: 211 | #Exception other than TypeError probably missing key, so default 212 | return default 213 | return tmp 214 | 215 | 216 | class Pluralizer(): 217 | # 218 | # (pattern, search, replace) regex english plural rules tuple 219 | # 220 | rule_tuple = ( 221 | ('[ml]ouse$', '([ml])ouse$', '\\1ice'), 222 | ('child$', 'child$', 'children'), 223 | ('booth$', 'booth$', 'booths'), 224 | ('foot$', 'foot$', 'feet'), 225 | ('ooth$', 'ooth$', 'eeth'), 226 | ('l[eo]af$', 'l([eo])af$', 'l\\1aves'), 227 | ('sis$', 'sis$', 'ses'), 228 | ('man$', 'man$', 'men'), 229 | ('ife$', 'ife$', 'ives'), 230 | ('eau$', 'eau$', 'eaux'), 231 | ('lf$', 'lf$', 'lves'), 232 | ('[sxz]$', '$', 'es'), 233 | ('[^aeioudgkprt]h$', '$', 'es'), 234 | ('(qu|[^aeiou])y$', 'y$', 'ies'), 235 | ('$', '$', 's') 236 | ) 237 | 238 | def regex_rules(self, rules=rule_tuple): 239 | for line in rules: 240 | pattern, search, replace = line 241 | yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) 242 | 243 | def plural(self, noun): 244 | for rule in self.regex_rules(): 245 | result = rule(noun) 246 | if result: 247 | return result 248 | 249 | 250 | class Cycler(): 251 | cycle_registry = {} 252 | 253 | @staticmethod 254 | def uuid(): 255 | import uuid 256 | return uuid.uuid1() 257 | 258 | @staticmethod 259 | def cycle(values, name='default'): 260 | if name in Cycler.cycle_registry: 261 | try: 262 | return Cycler.cycle_registry[name].next() 263 | except StopIteration: 264 | Cycler.cycle_registry[name] = iter(values) 265 | return Cycler.cycle_registry[name].next() 266 | else: 267 | Cycler.cycle_registry[name] = iter(values) 268 | return Cycler.cycle_registry[name].next() 269 | -------------------------------------------------------------------------------- /whirlwind/conf/app_template/static/js/whirlwind.js: -------------------------------------------------------------------------------- 1 | var Whirlwind = {}; 2 | 3 | //test for the console.. 4 | window.log = function f(){ log.history = log.history || []; log.history.push(arguments); if(this.console) { var args = arguments, newarr; args.callee = args.callee.caller; newarr = [].slice.call(args); if (typeof console.log === 'object') log.apply.call(console.log, console, newarr); else console.log.apply(console, newarr);}}; 5 | (function(a){function b(){}for(var c="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),d;!!(d=c.pop());){a[d]=a[d]||b;}}) 6 | (function(){try{console.log();return window.console;}catch(a){return (window.console={});}}()); 7 | 8 | /** 9 | * assumes the template has named variable in the form 10 | * 11 | * will unescape {} symbols, as firefox mistakenly fucks things up sometimes. 12 | * 13 | * {varName} and has a corresponding value in teh options 14 | */ 15 | Whirlwind.evalTemplate = function(htmlTemplate, options) { 16 | var syntax = /(\{(\w+)\})/g; //matches symbols like '{id}' 17 | 18 | if (!options) { 19 | return htmlTemplate; 20 | } 21 | 22 | if (!Whirlwind.isString(htmlTemplate)) { 23 | return htmlTemplate; 24 | } 25 | htmlTemplate = htmlTemplate.replace('%7B', '{').replace('%7D', '}'); 26 | htmlTemplate = htmlTemplate.replace('%7b', '{').replace('%7d', '}'); 27 | var html = htmlTemplate.replace(syntax, function(match,group1) { 28 | var tmp = match.replace('{',''); 29 | tmp = tmp.replace('}',''); 30 | if (typeof options[tmp] != 'undefined' && options[tmp] != null) { 31 | return options[tmp]; 32 | } 33 | return match; 34 | }); 35 | return html; 36 | }; 37 | 38 | 39 | Whirlwind.isFunction = function(obj) { 40 | //console.log("testing is function %o", obj); 41 | if (!obj) { 42 | return false; 43 | } 44 | return $.isFunction(obj); 45 | }; 46 | 47 | Whirlwind.isString= function(obj) { 48 | if (!obj) { 49 | return false; 50 | } 51 | if (obj instanceof String) { 52 | return true; 53 | } 54 | if (typeof(obj) == 'string') { 55 | return true; 56 | } 57 | return false; 58 | }; 59 | 60 | 61 | Whirlwind.isNumber = function(obj) { 62 | return typeof(obj) == 'number'; 63 | }; 64 | 65 | Whirlwind.isTrue = function(obj) { 66 | if (!obj) { 67 | return false; 68 | } 69 | if (obj == 'true') { 70 | return true; 71 | } 72 | if (obj == true) { 73 | return true; 74 | } 75 | //TODO;check if this is checkbox 76 | 77 | return false; 78 | } 79 | 80 | Whirlwind.isArray = function(obj) { 81 | if (!obj) { 82 | return false; 83 | } 84 | return $.isArray(obj); 85 | }; 86 | 87 | /** 88 | * Parses an iso date. 89 | * 90 | * works with most (all?) iso format variations 91 | * 2011-04-25T20:59:59.999-07:00 92 | * 2011-04-25T20:59:59+07:00 93 | * 2011-04-25T20:59:59Z 94 | * 95 | */ 96 | Whirlwind.parseISODate = function(string) { 97 | // http://webcloud.se/log/JavaScript-and-ISO-8601/ 98 | //Fixed the incorrect escaping from the original blog post. 99 | var regexp = "([0-9]{4})(\\-([0-9]{2})(\\-([0-9]{2})" + 100 | "(T([0-9]{2}):([0-9]{2})(\\:([0-9]{2})(\\.([0-9]+))?)?" + 101 | "(Z|(([\\-\\+])([0-9]{2})\\:?([0-9]{2})))?)?)?)?"; 102 | var d = string.match(new RegExp(regexp)); 103 | if (d == null || d.length == 0) { 104 | return null; 105 | } 106 | var offset = 0; 107 | var date = new Date(d[1], 0, 1); 108 | 109 | if (d[3]) { date.setMonth(d[3] - 1); } 110 | if (d[5]) { date.setDate(d[5]); } 111 | if (d[7]) { date.setHours(d[7]); } 112 | if (d[8]) { date.setMinutes(d[8]); } 113 | if (d[10]) { date.setSeconds(d[10]); } 114 | 115 | if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); } 116 | 117 | if (d[14]) { 118 | offset = (Number(d[16]) * 60) + Number(d[17]); 119 | console.log(offset); 120 | offset *= ((d[15] == '-') ? 1 : -1); 121 | } 122 | 123 | offset -= date.getTimezoneOffset(); 124 | time = (Number(date) + (offset * 60 * 1000)); 125 | return new Date(Number(time)); 126 | } 127 | 128 | 129 | /** 130 | * Will convert passed in element to a date (when possible) 131 | * 132 | * this requires the date.js to work. 133 | * will successfully parse iso dates of all styles. 134 | * 135 | */ 136 | Whirlwind.toDate = function(date) { 137 | if (!date) { 138 | return null; 139 | } 140 | if (date instanceof Date) { 141 | return date; 142 | } 143 | 144 | //ISO date from above 145 | var d = Whirlwind.parseISODate(date); 146 | if (d) { 147 | return d; 148 | } 149 | return new Date(date *1000); 150 | }; 151 | 152 | 153 | /** 154 | * Serializes the passed in element into an object. 155 | * uses all input elements below the passed in element. 156 | * 157 | * 158 | */ 159 | Whirlwind.serialize = function(element) { 160 | var params = {}; 161 | $(element).find(':input').each(function(i) { 162 | if (!($(this).data('iptrMsgDisplayed')) && 163 | this.name && !this.disabled && 164 | (this.checked || /select|textarea/i.test(this.nodeName) || 165 | /text|hidden|password/i.test(this.type))) { 166 | if (params[this.name]) { 167 | if (!Whirlwind.isArray(params[this.name])) { 168 | var tmp = params[this.name]; 169 | params[this.name] = []; 170 | params[this.name].push(tmp); 171 | } 172 | params[this.name].push($(this).val()); 173 | } else { 174 | params[this.name] = $(this).val(); 175 | } 176 | } 177 | 178 | }); 179 | return params; 180 | }; 181 | 182 | 183 | /** 184 | * gets the value from an input element, 185 | * or sets the value if passed in 186 | * returns the value 187 | */ 188 | Whirlwind.value = function(elem, value) { 189 | if ($(elem).size() == 0) 190 | return; 191 | var dom = $(elem).get(0) 192 | if (typeof value != 'undefined') { 193 | dom.value = value; 194 | return value; 195 | } else { 196 | if($(elem).attr('type') == 'checkbox') { 197 | if (dom.checked) 198 | return true; 199 | return false; 200 | } else if ($(elem).attr('type') == 'radio') { 201 | return $(elem).val(); 202 | } 203 | return dom.value; 204 | } 205 | }; 206 | 207 | /** 208 | * trims off whitespace from front and end of string, 209 | * if second param is specified will trim that off instead of whitespace. 210 | * 211 | * ex: 212 | * Trendrr.trim(' string '); => 'string' 213 | * Trendrr.trim('trendrr', 'r'); => 'trend' 214 | */ 215 | Whirlwind.trim = function(str, trim) { 216 | if (!str) { 217 | return str; 218 | } 219 | if (!trim) { 220 | return jQuery.trim(str); 221 | } 222 | var tmp = str; 223 | while(Whirlwind.stringStartsWith(tmp, trim)) { 224 | tmp = tmp.slice(trim.length); 225 | } 226 | while(Whirlwind.stringEndsWith(tmp, trim)) { 227 | tmp = tmp.slice(0, tmp.length-trim.length); 228 | } 229 | return tmp; 230 | }; 231 | 232 | /** 233 | * 234 | * Will take a string or dict as params. 235 | * Trendrr.href(urlString) will change the page to the passed in url. 236 | * 237 | * options 238 | * url => url to work from (default to window location) 239 | * params => will replace any params in the query string 240 | * forward => (default true)if true will not return, but rather forward browser to the newly generated url. 241 | * if false will return the updated url 242 | * clearParams = (default false) if true will clear all url params before the params are applied 243 | */ 244 | Whirlwind.href = function(options) { 245 | if (Whirlwind.isString(options)) { 246 | options = {url:options}; 247 | } 248 | var opts = $.extend({ 249 | url : window.location.href, 250 | forward : true, 251 | clearParams : false, 252 | params : {} 253 | }, options); 254 | 255 | var u = opts.url; 256 | 257 | //strip local link 258 | u = u.split('#')[0]; 259 | if (opts.clearParams) { 260 | u = u.split('?')[0]; 261 | } 262 | 263 | 264 | if (u.indexOf('?') == -1) { 265 | u += '?'; 266 | } 267 | $.each(opts.params, function(i, val) { 268 | var regex = new RegExp('\\&*' + i + '=[^\\&]*'); 269 | u = u.replace(regex, ''); 270 | u += '&' + i + '=' + this; 271 | }); 272 | console.log(opts); 273 | if (opts.forward) { 274 | window.location.href = u; 275 | } else { 276 | return u; 277 | } 278 | }; 279 | 280 | 281 | /** 282 | * tests wether the element is currently visible 283 | * to be visible the element must not have display:none, and 284 | * must be with the current scrollpane., 285 | */ 286 | 287 | Whirlwind.isVisible = function(element) { 288 | if (!element) { 289 | return false; 290 | } 291 | var elem = $(element); 292 | 293 | if (elem.size() == 0) { 294 | return false; 295 | } 296 | 297 | if (!elem.is(':visible')) { 298 | return false; 299 | } 300 | 301 | if (elem.hasClass('ui-offscreen')) { 302 | return false; 303 | } 304 | 305 | if (elem.closest('.ui-offscreen').size() > 0) { 306 | return false; 307 | } 308 | 309 | var measure = Whirlwind.measure(elem); 310 | 311 | if ($(document).scrollTop() > (measure.top + measure.height)) { 312 | return false; 313 | } 314 | if ((measure.left + measure.width) < 1) { 315 | return false; 316 | } 317 | return true; 318 | }; 319 | 320 | /** 321 | * formats numbers 322 | * like 323 | * 1.5M 324 | * 1,000 325 | * 10,000 326 | * 1.2K 327 | * 328 | * 329 | */ 330 | Whirlwind.formatNumber = function(number) { 331 | var str = ''; 332 | var unit = ''; 333 | 334 | if (number < 10000) { 335 | str = number + ''; //TODO: add comma 336 | } else if (number < 1000000) { 337 | var val = number / 1000; 338 | str = val.toPrecision(4) + "K"; 339 | unit = "K"; 340 | } else if (number < 1000000000) { 341 | var val = number / 1000000; 342 | str = val.toPrecision(4) + "M"; 343 | unit = "M"; 344 | } else { 345 | //billions 346 | var val = number / 1000000000; 347 | str = val.toPrecision(4) + "B"; 348 | unit = "B"; 349 | } 350 | 351 | str = str.replace(/\.0+[A-Za-z]+/, ''); 352 | str = str.replace(/(\.[1-9]*)0+[A-Za-z]+/, '$1'); 353 | 354 | return str + unit; 355 | 356 | }; -------------------------------------------------------------------------------- /whirlwind/core/request.py: -------------------------------------------------------------------------------- 1 | from tornado.web import _unicode 2 | from tornado.web import RequestHandler, HTTPError 3 | from mako.template import Template 4 | from mako.lookup import TemplateLookup 5 | from tornado.options import options 6 | from tornado import escape 7 | import datetime 8 | import re, sys, threading, os, httplib, tornado.web 9 | from urllib import unquote 10 | from whirlwind.middleware import MiddlewareManager 11 | from whirlwind.core.log import Log 12 | from tornado.web import ErrorHandler 13 | from tornado import ioloop 14 | from pymongo import * 15 | from whirlwind.view.filters import Filters 16 | from whirlwind.view.paginator import Paginator 17 | from whirlwind.core import dotdict 18 | from whirlwind.db.mongo import Mongo 19 | import pymongo 20 | from tornado.websocket import WebSocketHandler 21 | 22 | class WebSocketBaseRequest(WebSocketHandler): 23 | 24 | def __init__(self, application, request): 25 | WebSocketHandler.__init__(self, application, request) 26 | self._current_user = None 27 | self.middleware_manager = MiddlewareManager(self) 28 | self.db = Mongo.db.ui #@UndefinedVariable 29 | #run all middleware request hooks 30 | self.middleware_manager.run_request_hooks() 31 | 32 | def get_current_user(self): 33 | return self._current_user 34 | 35 | def set_current_user(self, user): 36 | self._current_user = user 37 | 38 | def is_logged_in(self): 39 | return self.get_current_user() != None 40 | 41 | 42 | class BaseRequest(RequestHandler): 43 | 44 | __template_exists_cache = {} 45 | 46 | def __init__(self, application, request): 47 | RequestHandler.__init__(self, application, request) 48 | self._current_user = None 49 | self.middleware_manager = MiddlewareManager(self) 50 | self._is_threaded = False 51 | self._is_whirlwind_finished = False 52 | self.view = dotdict() 53 | self.db = Mongo.db.ui #@UndefinedVariable 54 | 55 | def template_exists(self, template_name): 56 | tmp = self.__template_exists_cache.get(template_name, None) 57 | if tmp != None: 58 | print "found in cache: " + template_name 59 | return tmp 60 | 61 | lookup = self._get_template_lookup() 62 | try: 63 | new_template = lookup.get_template(template_name) 64 | if new_template : 65 | self.__template_exists_cache[template_name] = True 66 | return True 67 | except Exception as detail: 68 | print 'run-time error in BaseRequest::template_exists - ', detail 69 | self.__template_exists_cache[template_name] = False 70 | return False 71 | 72 | 73 | def _get_template_lookup(self,extra_imports=None) : 74 | from whirlwind.view.filters import Cycler 75 | Cycler.cycle_registry = {} 76 | 77 | filter_imports=[ 78 | 'from whirlwind.view.filters import Filters, Cycler', 79 | ] 80 | 81 | if extra_imports: 82 | filter_imports.extend(extra_imports) 83 | 84 | 85 | if isinstance(options.template_dir,(list,tuple)): 86 | directory_paths = options.template_dir 87 | else: 88 | directory_paths = [options.template_dir] 89 | 90 | return TemplateLookup( 91 | directories=directory_paths, 92 | module_directory=options.mako_modules_dir, 93 | output_encoding='utf-8', 94 | encoding_errors='replace', 95 | imports=filter_imports 96 | ) 97 | 98 | 99 | #to support backwards compat 100 | def render_to_string(self,template_name,**kwargs): 101 | self.render_to_string(template_name,**kwargs) 102 | 103 | #returns the rendered output of a template populated with kwargs 104 | def render_string(self,template_name,**kwargs): 105 | lookup = self._get_template_lookup() 106 | new_template = lookup.get_template(template_name) 107 | kwargs = self.add_context_vars(**kwargs) 108 | return new_template.render(**kwargs) 109 | 110 | def add_context_vars(self,**kwargs): 111 | tornado_args = { 112 | "_": self.locale.translate, 113 | "current_user": self.get_current_user(), 114 | "datetime": datetime, 115 | "escape": escape.xhtml_escape, 116 | "handler": self, 117 | "json_encode": escape.json_encode, 118 | "linkify": escape.linkify, 119 | "locale": self.locale, 120 | "request": self.request, 121 | "reverse_url": self.application.reverse_url, 122 | "squeeze": escape.squeeze, 123 | "static_url": self.static_url, 124 | "url_escape": escape.url_escape, 125 | "xhtml_escape": escape.xhtml_escape, 126 | "xsrf_form_html": self.xsrf_form_html 127 | } 128 | tornado_args.update(self.ui) 129 | 130 | whirlwind_args = { 131 | "is_logged_in": self.get_current_user() != None, 132 | "render_as": self.get_argument("render_as", "html"), 133 | "dict_get" : Filters.dict_get 134 | } 135 | 136 | kwargs.update(whirlwind_args) 137 | kwargs.update(tornado_args) 138 | kwargs.update(self.view) 139 | 140 | return kwargs 141 | 142 | def render_template(self,template_name, **kwargs): 143 | lookup = self._get_template_lookup() 144 | new_template = lookup.get_template(template_name) 145 | 146 | kwargs = self.add_context_vars(**kwargs) 147 | 148 | self.middleware_manager.run_view_hooks(view=kwargs) 149 | 150 | self.finish(new_template.render(**kwargs)) 151 | 152 | def get_nested_argument(self,prefix): 153 | ''' 154 | get nested form input params as an object: project[name] 155 | ''' 156 | 157 | params = self.get_arguments_as_dict() 158 | param_obj = {} 159 | 160 | for key in params.keys(): 161 | if key.startswith(prefix): 162 | if '[' in key and ']' in key: 163 | param_obj[key[key.find("[")+1:key.find("]")]] = params[key] 164 | 165 | return param_obj 166 | 167 | ''' 168 | hook into the end of the request 169 | ''' 170 | def finish(self, chunk=None): 171 | self._is_whirlwind_finished = True 172 | #run all middleware response hooks 173 | self.middleware_manager.run_response_hooks() 174 | if self._is_threaded : 175 | 176 | print "Thread finished. setting ioloop callback..", str(threading.currentThread()) 177 | self._chunk = chunk 178 | ioloop.IOLoop.instance().add_callback(self.threaded_finish_callback) 179 | return 180 | 181 | super(BaseRequest, self).finish(chunk) 182 | 183 | 184 | ''' 185 | this is called by the ioloop when the thread finally returns. 186 | ''' 187 | def threaded_finish_callback(self): 188 | print "In the finish callback thread is ", str(threading.currentThread()) 189 | super(BaseRequest, self).finish(self._chunk) 190 | self._chunk = None; 191 | 192 | 193 | 194 | ''' 195 | hook into the begining of the request here 196 | ''' 197 | def prepare(self): 198 | #run all middleware request hooks 199 | self.middleware_manager.run_request_hooks() 200 | 201 | def get_current_user(self): 202 | return self._current_user 203 | 204 | def set_current_user(self, user): 205 | self._current_user = user 206 | 207 | def is_logged_in(self): 208 | return self.get_current_user() != None 209 | 210 | ''' 211 | gets all the request params as a map. cleans them all up ala get_argument(s) 212 | ''' 213 | def get_arguments_as_dict(self): 214 | params = {} 215 | for key in self.request.arguments: 216 | values = self.get_arguments(key) 217 | k = unquote(key) 218 | if len(values) == 1 : 219 | params[k] = values[0] 220 | else : 221 | params[k] = values 222 | 223 | return params 224 | 225 | ''' 226 | Same as get_argument but will return a list 227 | if no arguments are supplied then a dict of all 228 | the arguments is returned. 229 | ''' 230 | def get_arguments(self, name=None, default=None, strip=True): 231 | if name is None : 232 | return self.get_arguments_as_dict() 233 | 234 | values = self.request.arguments.get(name, None) 235 | if values is None: 236 | return default 237 | 238 | retVals = [] 239 | for val in values : 240 | value = self._cleanup_param(val, strip) 241 | retVals.append(value) 242 | return retVals 243 | 244 | def get_argument(self, name, default=RequestHandler._ARG_DEFAULT, strip=True): 245 | value = super(BaseRequest, self).get_argument(name, default, strip) 246 | if value == default : 247 | return value 248 | return unquote(value) 249 | 250 | ''' 251 | cleans up any argument 252 | removes control chars, unescapes, ect 253 | ''' 254 | def _cleanup_param(self, val, strip=True): 255 | # Get rid of any weird control chars 256 | value = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", val) 257 | value = _unicode(value) 258 | if strip: value = value.strip() 259 | return unquote(value) 260 | 261 | def get_username(self): 262 | if self.get_current_user() : 263 | return self.get_current_user()['_id'] 264 | return None 265 | 266 | 267 | def write(self,chunk,status=None): 268 | if status: 269 | self.set_status(status) 270 | 271 | RequestHandler.write(self, chunk) 272 | 273 | def get_error_html(self, status_code, **kwargs): 274 | error_handler = WhirlwindErrorHandler(self.application, self.request, status_code=status_code) 275 | return error_handler.get_error_html(status_code, **kwargs) 276 | 277 | class WhirlwindErrorHandler(ErrorHandler): 278 | def get_error_html(self, status_code, **kwargs): 279 | self.require_setting("static_path") 280 | if status_code in [404, 500, 503, 403]: 281 | filename = os.path.join(self.settings['static_path'], 'errors/%d.html' % status_code) 282 | if os.path.exists(filename): 283 | f = open(filename, 'r') 284 | data = f.read() 285 | f.close() 286 | return data 287 | return "