├── .gitignore ├── INSTALL.txt ├── README.md ├── TODO.txt ├── app.py ├── apps ├── __init__.py ├── gists │ ├── __init__.py │ ├── handlers.py │ ├── indexes.py │ ├── migrations │ │ ├── 001.gist_basic_indexes.py │ │ └── __init__.py │ ├── models.py │ └── tests │ │ ├── __init__.py │ │ ├── test_handlers.py │ │ └── test_models.py ├── main │ ├── __init__.py │ ├── handlers.py │ ├── indexes.py │ ├── migrations │ │ ├── 000.example.py.txt │ │ ├── 001.basic_indexes.py │ │ └── always_gists.py │ ├── models.py │ ├── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── mock_data.py │ │ ├── test_handlers.py │ │ ├── test_models.py │ │ └── test_utils.py │ └── ui_modules.py ├── templates │ ├── base.html │ ├── gist.html │ ├── gist_not_found.html │ ├── home.html │ ├── modules │ │ └── footer.html │ ├── user │ │ └── sharing.html │ └── voting │ │ ├── most_loved.html │ │ └── most_loved_user.html └── voting │ ├── __init__.py │ ├── handlers.py │ ├── indexes.py │ ├── models.py │ └── tests │ ├── __init__.py │ └── test_handlers.py ├── bin ├── _run_coverage_tests.py ├── _run_tests.py ├── ensure_indexes.py ├── recalculate_voting_points.py ├── run_coverage_tests.sh ├── run_development_server.sh ├── run_migrations.py ├── run_pyflakes.py ├── run_shell.py ├── run_tests.sh └── update-user-details.py ├── external_apps.txt ├── find_console.log.sh ├── settings.py ├── static ├── css │ ├── base.css │ ├── ext │ │ ├── fancybox │ │ │ ├── blank.gif │ │ │ ├── fancy_close.png │ │ │ ├── fancy_loading.png │ │ │ ├── fancy_nav_left.png │ │ │ ├── fancy_nav_right.png │ │ │ ├── fancy_shadow_e.png │ │ │ ├── fancy_shadow_n.png │ │ │ ├── fancy_shadow_ne.png │ │ │ ├── fancy_shadow_nw.png │ │ │ ├── fancy_shadow_s.png │ │ │ ├── fancy_shadow_se.png │ │ │ ├── fancy_shadow_sw.png │ │ │ ├── fancy_shadow_w.png │ │ │ ├── fancy_title_left.png │ │ │ ├── fancy_title_main.png │ │ │ ├── fancy_title_over.png │ │ │ ├── fancy_title_right.png │ │ │ ├── fancybox-x.png │ │ │ ├── fancybox-y.png │ │ │ ├── fancybox.png │ │ │ └── jquery.fancybox-1.3.4.css │ │ ├── indicator.gif │ │ ├── jquery.autocomplete.css │ │ ├── jquery.qtip.css │ │ └── jquery.qtip.min.css │ └── extra.css ├── images │ ├── comment.png │ ├── favicon.ico │ ├── heart16.png │ ├── heart22.png │ ├── heart48.png │ └── tornado.png └── js │ ├── ext │ ├── jquery-1.5.1.min.js │ ├── jquery.autocomplete.pack.js │ ├── jquery.qtip.js │ ├── jquery.qtip.min.js │ └── jquery.qtip.pack.js │ └── gist.js └── utils ├── __init__.py ├── decorators.py ├── git.py ├── http_test_client.py ├── routes.py ├── send_mail ├── __init__.py ├── backends │ ├── __init__.py │ ├── base.py │ ├── console.py │ ├── locmem.py │ └── smtp.py ├── config.py ├── dns_name.py ├── importlib.py └── send_email.py ├── timesince.py ├── truncate.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | coverage_report/ 2 | *~ 3 | *.pyc 4 | .venv 5 | run_mongodb.sh 6 | start_mongod.sh 7 | db/ 8 | gkc-db/ 9 | mongodb 10 | tags 11 | *.py.done 12 | build/ 13 | pip-log.txt 14 | run_mongo_data_browser.sh 15 | mongo_data_browser/ 16 | .base64-image-conversions.pickle 17 | marketing/emails.csv 18 | _run_all_static_modules2.py 19 | _run_all_static_modules.py 20 | _run_all_static_modules2.py 21 | .static_name_conversion 22 | mail.txt 23 | cdn_prefix.conf 24 | twitter_consumer_secret 25 | *.old 26 | gkc.pem 27 | gists-db 28 | local_settings.py 29 | github_client_secret 30 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | INSTALLATION 2 | ============ 3 | 4 | Tornado 5 | ------- 6 | 7 | Install latest tornado from github. 8 | 9 | 10 | Python Dependencies 11 | ------------------- 12 | 13 | All pip installable apps are mentioned in `external_apps.txt` 14 | 15 | $ pip install -r external_apps.txt 16 | 17 | Linux dependencies 18 | ------------------ 19 | 20 | mongodb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About tornado_gists 2 | =================== 3 | 4 | This is a [Tornado](http://www.tornadoweb.org/) web app that integrates with 5 | [Github Gists](https://gist.github.com/) so that people in the Tornado 6 | community can share snippets of code with each other. 7 | 8 | Anybody can log in (using their Github account) and start adding gists 9 | and discuss them. **Not** only **their own** but anybody's gist. 10 | 11 | 12 | About the code 13 | -------------- 14 | 15 | All the code is of course Open Source and is licensed under the [Apache 16 | Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 17 | 18 | It's by no means perfect and absolutely not the ideal way of writing a 19 | Tornado app (because there is no such way). 20 | 21 | To run the code you simply need a MongoDB (of a recentish version) and 22 | install MongoKit which glues MongoDB to Tornado. 23 | 24 | 25 | About running the tests 26 | ----------------------- 27 | 28 | To run the tests simply run this command: 29 | 30 | ./bin/run_tests.sh 31 | 32 | If you want to run tests just for one single app, `gists` run this: 33 | 34 | ./bin/run_tests.sh apps.gists.tests 35 | 36 | And to run a specific test: 37 | 38 | ./bin/run_tests.sh \ 39 | apps.main.tests.test_handlers.HandlersTestCase.test_homepage 40 | 41 | If you're doing development and want to run the same test(s) over and 42 | over just add `--autoreload` like this: 43 | 44 | ./bin/run_tests.sh --autoreload apps.gists.tests 45 | 46 | To run the coverage tests, make sure you have ``coverage`` installed 47 | and run this script: 48 | 49 | ./bin/run_coverage_tests.sh 50 | 51 | At the moment, this covers 87% of all the code on my computer. 52 | 53 | 54 | 55 | About running database migrations 56 | --------------------------------- 57 | 58 | MongoDB has no problem with some documents having different structure 59 | even if they're in the same collection. However, the glue code we use 60 | has. MongoKit expects every document in a collection to have the same 61 | structure. At least, every document must have everything that is 62 | entered in the class attribute `structure` in the document classes. 63 | 64 | To add migration code you can do two things. For simple changes create 65 | a file called `/migrations/always_.py` these files 66 | are executed whenever you run the script that starts it: 67 | 68 | ./bin/run_migrations.py 69 | 70 | If you have a specific change you want to make create a file that 71 | starts with a number like this for example `003.gender_on_user.py` and 72 | stick it in the `migrations` directory. Next time you run 73 | `run_migrations` it will execute the file and create a file called 74 | `003.gender_on_user.py.done` so that you don't accidentally run the 75 | file again. All `*.done` files are supposed to be ignored by git and 76 | not added to the repo. 77 | 78 | 79 | 80 | About indexing the MongoDB 81 | -------------------------- 82 | 83 | Indexing can be a precious operation and has therefore been removed 84 | from the code as an automated task. Instead, has to be executed 85 | manually. It's only really necessary to run once since MongoDB is 86 | smart enough to maintain the indexes for new documents once everything 87 | is set up. If you haven't already done so, to ensure all the indexes 88 | run this: 89 | 90 | ./bin/run_migrations.py 91 | 92 | Adding more indexing is basically the same as running migrations. 93 | 94 | 95 | Updating user details 96 | --------------------- 97 | 98 | To update the user details, run the script 99 | ``bin/update-user-details.py`` periodically. You can specify how many 100 | updates to make maximum. Default is 100. Example usage: 101 | 102 | ./bin/update-user-details.py 103 | 104 | 105 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | * commenting on lines of code 2 | 3 | * Comments in HTML instead of AJAX (SEO) 4 | 5 | * Ability to tag gists 6 | 7 | * Ability to pull in existing comments gists -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | 4 | 5 | # python 6 | import types 7 | import re 8 | import os.path 9 | import logging 10 | from mongokit import Connection, Document as mongokit_Document 11 | 12 | # tornado 13 | import tornado.httpserver 14 | import tornado.ioloop 15 | import tornado.options 16 | import tornado.web 17 | from tornado.options import define, options 18 | 19 | # app 20 | import settings 21 | from utils.routes import route 22 | from utils.git import get_git_revision 23 | 24 | ################################################################################ 25 | 26 | define("debug", default=False, help="run in debug mode", type=bool) 27 | define("port", default=8000, help="run on the given port", type=int) 28 | define("database_name", default=settings.DEFAULT_DATABASE_NAME, help="database name") 29 | define("prefork", default=False, help="pre-fork across all CPUs", type=bool) 30 | define("showurls", default=False, help="Show all routed URLs", type=bool) 31 | define("dont_combine", default=False, help="Don't combine static resources", type=bool) 32 | 33 | 34 | class Application(tornado.web.Application): 35 | def __init__(self, 36 | database_name=None, 37 | xsrf_cookies=True, 38 | optimize_static_content=None): 39 | ui_modules_map = {} 40 | for app_name in settings.APPS: 41 | _ui_modules = __import__('apps.%s' % app_name, globals(), locals(), 42 | ['ui_modules'], -1) 43 | try: 44 | ui_modules = _ui_modules.ui_modules 45 | except AttributeError: 46 | # this app simply doesn't have a ui_modules.py file 47 | continue 48 | 49 | for name in [x for x in dir(ui_modules) if re.findall('[A-Z]\w+', x)]: 50 | thing = getattr(ui_modules, name) 51 | try: 52 | if issubclass(thing, tornado.web.UIModule): 53 | ui_modules_map[name] = thing 54 | except TypeError: 55 | # most likely a builtin class or something 56 | pass 57 | 58 | try: 59 | cdn_prefix = [x.strip() for x in file('cdn_prefix.conf') 60 | if x.strip() and not x.strip().startswith('#')][0] 61 | #logging.info("Using %r as static URL prefix" % cdn_prefix) 62 | except (IOError, IndexError): 63 | cdn_prefix = None 64 | 65 | # unless explicitly set, then if in debug mode, disable optimization 66 | # of static content 67 | if optimize_static_content is None: 68 | optimize_static_content = not options.debug 69 | 70 | handlers = route.get_routes() 71 | app_settings = dict( 72 | title=settings.TITLE, 73 | template_path=os.path.join(os.path.dirname(__file__), "apps", "templates"), 74 | static_path=os.path.join(os.path.dirname(__file__), "static"), 75 | ui_modules=ui_modules_map, 76 | xsrf_cookies=xsrf_cookies, 77 | cookie_secret=settings.COOKIE_SECRET, 78 | login_url=settings.LOGIN_URL, 79 | debug=options.debug, 80 | optimize_static_content=optimize_static_content, 81 | git_revision=get_git_revision(), 82 | email_backend=options.debug and \ 83 | 'utils.send_mail.backends.console.EmailBackend' \ 84 | or 'utils.send_mail.backends.smtp.EmailBackend', 85 | webmaster=settings.WEBMASTER, 86 | admin_emails=settings.ADMIN_EMAILS, 87 | CLOSURE_LOCATION=os.path.join(os.path.dirname(__file__), 88 | "static", "compiler.jar"), 89 | YUI_LOCATION=os.path.join(os.path.dirname(__file__), 90 | "static", "yuicompressor-2.4.2.jar"), 91 | cdn_prefix=cdn_prefix, 92 | ) 93 | tornado.web.Application.__init__(self, handlers, **app_settings) 94 | 95 | # Have one global connection to the blog DB across all handlers 96 | self.database_name = database_name and database_name or options.database_name 97 | self.con = Connection() # XXX: needs more options for host/username/password 98 | 99 | model_classes = [] 100 | for app_name in settings.APPS: 101 | _models = __import__('apps.%s' % app_name, globals(), locals(), 102 | ['models'], -1) 103 | try: 104 | models = _models.models 105 | except AttributeError: 106 | # this app simply doesn't have a models.py file 107 | continue 108 | for name in [x for x in dir(models) if re.findall('[A-Z]\w+', x)]: 109 | thing = getattr(models, name) 110 | if issubclass(thing, mongokit_Document): 111 | model_classes.append(thing) 112 | 113 | self.con.register(model_classes) 114 | 115 | 116 | 117 | for app_name in settings.APPS: 118 | __import__('apps.%s' % app_name, globals(), locals(), ['handlers'], -1) 119 | 120 | def main(): # pragma: no cover 121 | tornado.options.parse_command_line() 122 | if options.showurls: 123 | for each in route.get_routes(): 124 | print each._path.ljust(60), 125 | print each.handler_class.__name__ 126 | return 127 | 128 | http_server = tornado.httpserver.HTTPServer(Application()) 129 | print "Starting tornado on port", options.port 130 | if options.prefork: 131 | print "\tpre-forking" 132 | http_server.bind(options.port) 133 | http_server.start() 134 | else: 135 | http_server.listen(options.port) 136 | 137 | try: 138 | tornado.ioloop.IOLoop.instance().start() 139 | except KeyboardInterrupt: 140 | pass 141 | 142 | 143 | if __name__ == "__main__": 144 | main() 145 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- 1 | # perhaps more magic can be put here -------------------------------------------------------------------------------- /apps/gists/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/apps/gists/__init__.py -------------------------------------------------------------------------------- /apps/gists/handlers.py: -------------------------------------------------------------------------------- 1 | import markdown 2 | from dateutil.parser import parse as date_parse 3 | import anyjson 4 | from pprint import pprint 5 | import datetime 6 | import tornado.web 7 | from utils.routes import route, route_redirect 8 | from utils.timesince import smartertimesince 9 | from utils import gravatar_html 10 | from apps.main.handlers import BaseHandler 11 | 12 | 13 | @route(r'/add/$', name="add_gist") 14 | class AddGistHandler(BaseHandler): 15 | 16 | @tornado.web.asynchronous 17 | @tornado.web.authenticated 18 | def post(self): 19 | gist_id = int(self.get_argument('gist_id')) 20 | gist = self.db.Gist.one({'gist_id': gist_id}) 21 | if gist: 22 | return self.redirect(self.reverse_url('view_gist', gist_id)) 23 | http = tornado.httpclient.AsyncHTTPClient() 24 | url = "https://api.github.com/gists/%s" % gist_id 25 | http.fetch(url, callback=lambda r:self.on_gist_found(gist_id, r)) 26 | 27 | def on_gist_found(self, gist_id, response): 28 | gist_struct = anyjson.deserialize(response.body) 29 | pprint(gist_struct) 30 | try: 31 | gist_info = gist_struct#['gists'][0] 32 | except KeyError: 33 | # TODO: redirect to a warning page 34 | # gist is not found 35 | url = self.reverse_url("gist_not_found") 36 | url += '?gist_id=%s' % gist_id 37 | self.redirect(url) 38 | 39 | gist = self.db.Gist() 40 | gist.gist_id = gist_id 41 | gist.description = unicode(gist_info.get('description', u'')) 42 | #gist.created_at = date_parse(gist_info['created_at']) 43 | gist.created_at = unicode(gist_info['created_at']) 44 | gist.files = [unicode(x) for x in gist_info['files']] 45 | gist.contents = [] 46 | gist.public = gist_info['public'] 47 | gist.owner = unicode(gist_info['user']['login']) 48 | gist.repo = None #unicode(gist_info['repo']) 49 | gist.user = self.get_current_user() 50 | gist.save() 51 | 52 | self.redirect(self.reverse_url('edit_gist', gist.gist_id)) 53 | #files = iter(gist.files) 54 | #self.fetch_files(gist, files) 55 | 56 | def fetch_files(self, gist, files_iterator, response=None): # pragma: no cover 57 | # don't think we need this any more 58 | # Might be good one day for full text indexing search or something 59 | if response is not None: 60 | gist.contents.append(unicode(response.body)) 61 | gist.save() 62 | 63 | try: 64 | filename = files_iterator.next() 65 | http = tornado.httpclient.AsyncHTTPClient() 66 | url = "http://gist.github.com/raw/%s/%s" % (gist.gist_id, filename) # filename needs to be url quoted?? 67 | http.fetch(url, callback=lambda r:self.fetch_files(gist, files_iterator, r)) 68 | 69 | except StopIteration: 70 | self.redirect(self.reverse_url('view_gist', gist.gist_id)) 71 | 72 | @route(r'/notfound/$', name="gist_not_found") 73 | class GistNotFoundHandler(BaseHandler): 74 | @tornado.web.authenticated 75 | def get(self): 76 | options = self.get_base_options() 77 | options['gist_id'] = self.get_argument('gist_id', None) 78 | self.render("gist_not_found.html", **options) 79 | 80 | class GistBaseHandler(BaseHandler): 81 | 82 | def find_gist(self, gist_id): 83 | try: 84 | gist = self.db.Gist.one({'gist_id': int(gist_id)}) 85 | assert gist 86 | return gist 87 | except (ValueError, AssertionError): 88 | raise tornado.web.HTTPError(404, "Gist not found") 89 | 90 | 91 | @route(r'/(\d+)/$', name="view_gist") 92 | class GistHandler(GistBaseHandler): 93 | 94 | def get(self, gist_id): 95 | options = self.get_base_options() 96 | gist = self.find_gist(gist_id) 97 | assert gist.gist_id == int(gist_id) 98 | options['gist'] = gist 99 | options['edit'] = False 100 | _vote_search = {'gist.$id':gist._id, 'comment':None} 101 | gist_points = self.db.GistPoints.one({'gist.$id': gist._id}) 102 | if gist_points: 103 | options['vote_points'] = gist_points.points 104 | else: 105 | options['vote_points'] = 0 106 | 107 | options['has_voted_up'] = False 108 | if options['user']: 109 | _user_vote_search = dict(_vote_search) 110 | _user_vote_search['user.$id'] = options['user']._id 111 | options['has_voted_up'] = bool( 112 | self.db.Vote.collection.one(_user_vote_search)) 113 | self.render("gist.html", **options) 114 | 115 | 116 | @route(r'/(\d+)/edit/$', name="edit_gist") 117 | class EditGistHandler(GistHandler): 118 | 119 | @tornado.web.authenticated 120 | def get(self, gist_id): 121 | options = self.get_base_options() 122 | gist = self.find_gist(gist_id) 123 | if gist.user != options['user']: 124 | raise tornado.web.HTTPError(403, "Not your gist") 125 | options['gist'] = gist 126 | options['edit'] = True 127 | self.render("gist.html", **options) 128 | 129 | @tornado.web.authenticated 130 | def post(self, gist_id): 131 | options = self.get_base_options() 132 | gist = self.find_gist(gist_id) 133 | if gist.user != options['user']: 134 | raise tornado.web.HTTPError(403, "Not your gist") 135 | 136 | # fix 404 error if no description be posted 137 | # use gist_id as default description 138 | description = self.get_argument('description', u'GIST-%s' % gist_id).strip() 139 | discussion = self.get_argument('discussion', u'') 140 | tags = self.get_argument('tags', u'') 141 | tags = [x.strip() for x in tags.split(',') if x.strip()] 142 | 143 | try: 144 | # test if the markdown plain text isn't broken 145 | markdown.markdown(discussion, safe_mode="escape") 146 | except Exception: 147 | raise 148 | gist.description = description 149 | gist.discussion = discussion 150 | gist.discussion_format = u'markdown' 151 | gist.tags = tags 152 | gist.update_date = datetime.datetime.now() 153 | gist.save() 154 | url = self.reverse_url('view_gist', gist.gist_id) 155 | self.redirect(url) 156 | 157 | 158 | @route(r'/(\d+)/delete/$', name="delete_gist") 159 | class DeleteGistHandler(GistHandler): 160 | 161 | @tornado.web.authenticated 162 | def get(self, gist_id): 163 | gist = self.find_gist(gist_id) 164 | if gist.user != self.get_current_user(): 165 | raise tornado.web.HTTPError(403, "Not yours") 166 | 167 | # delete all comments of this gist 168 | for comment in self.db.Comment.find({'gist.$id': gist._id}): 169 | comment.delete() 170 | 171 | gist.delete() 172 | self.redirect('/?gist=deleted') 173 | 174 | 175 | @route(r'/preview_markdown$', name="preview_markdown") 176 | class PreviewMarkdownHandler(BaseHandler): 177 | def check_xsrf_cookie(self): 178 | pass 179 | 180 | def post(self): 181 | text = self.get_argument('text') 182 | html = markdown.markdown(text, safe_mode="escape") 183 | self.write_json(dict(html=html)) 184 | 185 | 186 | @route(r'/tags.json$', name="autocomplete_tags") 187 | class AutocompleteTags(BaseHandler): 188 | def check_xsrf_cookie(self): 189 | pass 190 | 191 | def get(self): 192 | search = self.get_argument('q', None) 193 | all_tags = set() 194 | all_tags_lower = set() 195 | search = {'tags': {'$ne': []}} 196 | for gist in self.db.Gist.collection.find(search): 197 | for tag in gist['tags']: 198 | if tag.lower() not in all_tags_lower: 199 | all_tags.add(tag) 200 | all_tags_lower.add(tag.lower()) 201 | all_tags = list(all_tags) 202 | all_tags.sort(lambda x,y:cmp(x.lower(), y.lower())) 203 | self.write_json(dict(tags=all_tags)) 204 | 205 | 206 | @route(r'/(\d+)/comments', name="comments") 207 | class CommentsHandler(GistHandler): 208 | 209 | def get(self, gist_id): 210 | #sg = ShowGravatar(self) 211 | gist = self.find_gist(gist_id) 212 | comments = [] 213 | now = datetime.datetime.now() 214 | for comment in self.db.Comment.find({'gist.$id': gist._id}).sort('add_date', 1): 215 | #pprint(dict(comment.user)) 216 | comment_dict = dict( 217 | comment=markdown.markdown(comment.comment, safe_mode='escape'), 218 | ago=smartertimesince(comment.add_date, now), 219 | file=comment.file, 220 | id=str(comment._id), 221 | user=dict(name=comment.user.name, 222 | login=comment.user.login, 223 | ) 224 | ) 225 | if comment.user.gravatar_id: 226 | comment_dict['user']['gravatar_html'] = \ 227 | gravatar_html(self.is_secure(), comment.user.gravatar_id, width_and_height=20) 228 | #pprint(comment_dict) 229 | comments.append(comment_dict) 230 | 231 | self.write_json(dict(comments=comments)) 232 | 233 | @tornado.web.authenticated 234 | def post(self, gist_id): 235 | options = self.get_base_options() 236 | gist = self.find_gist(gist_id) 237 | comment = self.get_argument('comment') 238 | file_ = self.get_argument('file') 239 | 240 | # check that the comment hasn't already been posted, 241 | # for example by double-clicking the submit button 242 | search = {'user.$id': options['user']._id, 243 | 'gist.$id': gist._id} 244 | for c in self.db.Comment.find(search): 245 | if c.comment == comment: 246 | break 247 | else: 248 | c = self.db.Comment() 249 | c.user = options['user'] 250 | c.gist = gist 251 | c.comment = comment 252 | c.comment_format = u'markdown' 253 | c.file = file_ 254 | c.save() 255 | 256 | url = self.reverse_url('view_gist', gist.gist_id) 257 | anchor = gist.files.index(file_) + 1 258 | self.redirect(url + "#comments-%s" % anchor) 259 | -------------------------------------------------------------------------------- /apps/gists/indexes.py: -------------------------------------------------------------------------------- 1 | from pymongo import ASCENDING, DESCENDING 2 | from models import Gist, Comment 3 | from mongokit import Connection 4 | import settings 5 | con = Connection() 6 | con.register([Gist, Comment]) 7 | db = con[settings.DEFAULT_DATABASE_NAME] 8 | 9 | def run(): 10 | collection = db.Gist.collection 11 | collection.ensure_index([('add_date',DESCENDING)]) 12 | yield 'add_date' 13 | collection.ensure_index('gist_id') 14 | yield 'gist_id' 15 | collection.ensure_index('tags') 16 | yield 'tags' 17 | collection.ensure_index('user.$id') # default ttl=300 18 | yield 'user.$id' 19 | 20 | collection = db.Comment.collection 21 | collection.ensure_index('user.$id') 22 | yield 'user.$id' 23 | collection.ensure_index('gist.$id') 24 | yield 'gist.$id' 25 | collection.ensure_index([('add_date',DESCENDING)]) 26 | yield 'add_date' 27 | 28 | test() 29 | 30 | 31 | def test(): 32 | curs = db.Gist.find().sort('add_date', DESCENDING).explain()['cursor'] 33 | assert 'BtreeCursor' in curs 34 | 35 | curs = db.Gist.find({'gist_id':'abc123'}).explain()['cursor'] 36 | assert 'BtreeCursor' in curs 37 | 38 | any_obj_id = list(db.Gist.find().limit(1))[0]._id 39 | curs = db.Gist.find({'user.$id':any_obj_id}).explain()['cursor'] 40 | assert 'BtreeCursor' in curs 41 | 42 | curs = db.Gist.find({'tags': {'$in':['abc123']}}).explain()['cursor'] 43 | assert 'BtreeCursor' in curs 44 | 45 | db.Comment.find({'user.$id': any_obj_id}).explain()['cursor'] 46 | assert 'BtreeCursor' in curs 47 | 48 | db.Comment.find({'gist.$id': any_obj_id}).explain()['cursor'] 49 | assert 'BtreeCursor' in curs 50 | -------------------------------------------------------------------------------- /apps/gists/migrations/001.gist_basic_indexes.py: -------------------------------------------------------------------------------- 1 | # Note: because this file is run from another python doing execfile() you might 2 | # suffer some weird behaviour if you try to call functions. 3 | 4 | from pymongo import ASCENDING, DESCENDING 5 | from apps.gists.models import Gist, Comment 6 | from mongokit import Connection 7 | import settings 8 | con = Connection() 9 | con.register([Gist, Comment]) 10 | db = con[settings.DEFAULT_DATABASE_NAME] 11 | 12 | collection = db.Gist.collection 13 | collection.ensure_index('gist_id', ttl=3000) # default ttl=300 14 | collection.ensure_index('user.$id') 15 | collection.ensure_index('add_date', direction=DESCENDING) 16 | 17 | # Test 18 | 19 | for gist in db.Gist.find(): 20 | curs = db.Gist.find({'gist_id': gist.gist_id}).explain()['cursor'] 21 | assert 'BtreeCursor' in curs 22 | curs = db.Gist.find({'tags': 'python'}).explain()['cursor'] 23 | assert 'BtreeCursor' in curs 24 | import re 25 | curs = db.Gist.find({'tags': re.compile('python', re.I)}).explain()['cursor'] 26 | assert 'BtreeCursor' in curs 27 | 28 | curs = db.Gist.find({'user.$id':gist.user._id}).explain()['cursor'] 29 | assert 'BtreeCursor' in curs 30 | break 31 | 32 | curs = db.Gist.find().sort('add_date', DESCENDING).limit(1).explain()['cursor'] 33 | assert 'BtreeCursor' in curs 34 | -------------------------------------------------------------------------------- /apps/gists/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/apps/gists/migrations/__init__.py -------------------------------------------------------------------------------- /apps/gists/models.py: -------------------------------------------------------------------------------- 1 | from pymongo.objectid import ObjectId 2 | import datetime 3 | from apps.main.models import BaseDocument, User 4 | 5 | 6 | class Gist(BaseDocument): 7 | __collection__ = 'gists' 8 | structure = { 9 | 'user': User, 10 | 'gist_id': int, 11 | # basic Gist information 12 | 'description': unicode, 13 | 'files':[unicode], 14 | 'created_at': unicode, #datetime.datetime, 15 | 'public': bool, 16 | 'owner': unicode, 17 | 'repo': unicode, 18 | # extra 19 | 'tags': [unicode], 20 | 'discussion': unicode, 21 | 'discussion_format': unicode, 22 | # persistent cache attributes 23 | 'contents': [unicode], 24 | } 25 | 26 | default_values = { 27 | 'discussion': u'', 28 | 'discussion_format': u'markdown', 29 | } 30 | 31 | @property 32 | def no_comments(self): 33 | _no_comments = getattr(self, '_no_comments', None) 34 | if _no_comments is None: 35 | _no_comments = self.db.Comment.find({'gist.$id':self._id}).count() 36 | self._no_comments = _no_comments 37 | return _no_comments 38 | 39 | 40 | class Comment(BaseDocument): 41 | __collection__ = 'comments' 42 | structure = { 43 | 'user': User, 44 | 'gist': Gist, 45 | 'comment': unicode, 46 | 'comment_format': unicode, 47 | 'file': unicode, 48 | 'line': int, 49 | 'reply_to': ObjectId 50 | } 51 | 52 | @property 53 | def file_index_number(self): 54 | if self.file: 55 | return self.gist.files.index(self.file) 56 | return 0 57 | -------------------------------------------------------------------------------- /apps/gists/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from test_models import * 2 | from test_handlers import * 3 | -------------------------------------------------------------------------------- /apps/gists/tests/test_handlers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from apps.main.tests.test_handlers import HandlersTestCase 3 | class GistsHandlersTestCase(HandlersTestCase): 4 | 5 | 6 | def test_commenting_on_gist(self): 7 | assert not self.db.Comment.find().count() 8 | peter = self.db.User() 9 | peter.login = u'peterbe' 10 | peter.email = u'' 11 | peter.company = u'' 12 | peter.name = u'Peter' 13 | peter.save() 14 | 15 | gist1 = self.db.Gist() 16 | gist1.user = peter 17 | gist1.gist_id = 1234 18 | gist1.created_at = u'yesterday' 19 | gist1.files = [u'foo.py'] 20 | gist1.description = u'Testing the Atom feed' 21 | gist1.discussion = u"""Full `markdown` description: 22 | function foo(): 23 | return bar 24 | 25 | [peterbe](http://www.peterbe.com) 26 | """ 27 | gist1.save() 28 | 29 | comment_url = self.reverse_url('comments', 1234) 30 | response = self.client.get(comment_url) 31 | assert response.code == 200 32 | struct = json.loads(response.body) 33 | self.assertEqual(struct, {'comments':[]}) 34 | 35 | post_data = { 36 | 'comment':'I love `cheese`!', 37 | 'file': 'foo.py', 38 | } 39 | response = self.client.post(comment_url, post_data) 40 | self.assertEqual(response.code, 403) 41 | 42 | self.client.login('peterbe') 43 | 44 | response = self.client.post(comment_url, post_data) 45 | self.assertEqual(response.code, 302) 46 | self.assertTrue(response.headers['Location'].startswith( 47 | self.reverse_url('view_gist', 1234))) 48 | 49 | assert self.db.Comment.find().count() 50 | comment = self.db.Comment.one() 51 | self.assertEqual(comment.user.login, 'peterbe') 52 | self.assertEqual(comment.comment, post_data['comment']) 53 | self.assertEqual(comment.file, post_data['file']) 54 | 55 | response = self.client.get(comment_url) 56 | assert response.code == 200 57 | struct = json.loads(response.body) 58 | self.assertEqual(len(struct['comments']), 1) 59 | first = struct['comments'][0] 60 | 61 | self.assertTrue(first.get('ago')) 62 | self.assertTrue('cheese' in first['comment']) 63 | self.assertTrue(first.get('user')) 64 | self.assertTrue(first.get('id')) 65 | 66 | def test_preview_markdown(self): 67 | url = self.reverse_url('preview_markdown') 68 | post_data = {'text': 69 | "Test\nfoo `and` "} 70 | response = self.client.post(url, post_data) 71 | assert response.code == 200 72 | struct = json.loads(response.body) 73 | assert struct['html'] 74 | self.assertTrue('and' in struct['html']) 75 | self.assertTrue( 76 | "<script>alert('xsss')</script>" \ 77 | in struct['html']) 78 | -------------------------------------------------------------------------------- /apps/gists/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from mongokit import RequireFieldError, ValidationError 2 | import datetime 3 | #import sys; sys.path.insert(0, '..') 4 | from apps.main.tests.base import BaseModelsTestCase 5 | from apps.gists.models import Gist, Comment 6 | #from apps.main.models import User 7 | 8 | class ModelsTestCase(BaseModelsTestCase): 9 | 10 | def setUp(self): 11 | super(ModelsTestCase, self).setUp() 12 | self.con.register([Gist, Comment]) 13 | 14 | 15 | def test_gist(self): 16 | user = self.db.User() 17 | user.login = u'peterbe' 18 | user.save() 19 | 20 | gist = self.db.Gist() 21 | gist.user = user 22 | gist.gist_id = 1 23 | gist.description = u'Description' 24 | gist.files = [u'foo.py', u'bar.js'] 25 | gist.save() 26 | 27 | assert gist.no_comments == 0 28 | 29 | def test_comment(self): 30 | user = self.db.User() 31 | user.login = u'peterbe' 32 | user.save() 33 | 34 | gist = self.db.Gist() 35 | gist.user = user 36 | gist.gist_id = 1 37 | gist.description = u'Description' 38 | gist.files = [u'foo.py', u'bar.js'] 39 | gist.save() 40 | 41 | comment = self.db.Comment() 42 | comment.gist = gist 43 | comment.user = user 44 | comment.comment = u"Works" 45 | comment.comment_format = u"markdown" 46 | comment.file = u'bar.js' 47 | comment.save() 48 | 49 | self.assertEqual(comment.file_index_number, 1) 50 | -------------------------------------------------------------------------------- /apps/main/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /apps/main/handlers.py: -------------------------------------------------------------------------------- 1 | # python 2 | import traceback 3 | import urllib 4 | import cgi 5 | from random import random 6 | import httplib 7 | from hashlib import md5 8 | from cStringIO import StringIO 9 | from urlparse import urlparse 10 | from pprint import pprint 11 | from collections import defaultdict 12 | from time import mktime, sleep 13 | import datetime 14 | from urllib import quote 15 | import os.path 16 | import re 17 | import logging 18 | import markdown 19 | 20 | from pymongo.objectid import InvalidId, ObjectId 21 | from pymongo import ASCENDING, DESCENDING 22 | 23 | try: 24 | import pyatom 25 | except ImportError: # pragma: no cover 26 | import warnings 27 | warnings.warn("pyatom not installed (pip install pyatom)") 28 | pyatom = None 29 | 30 | # tornado 31 | import tornado.auth 32 | import tornado.web 33 | import tornado.escape 34 | from tornado import httpclient 35 | 36 | # app 37 | from utils.routes import route, route_redirect 38 | from utils.send_mail import send_email 39 | from utils.decorators import login_required 40 | import settings 41 | 42 | 43 | class BaseHandler(tornado.web.RequestHandler): 44 | 45 | @property 46 | def db(self): 47 | return self.application.con[self.application.database_name] 48 | 49 | def is_secure(self): 50 | return self.request.headers.get('X-Scheme') == 'https' 51 | 52 | def xxx_handle_request_exception(self, exception): # pragma: no cover 53 | if not isinstance(exception, tornado.web.HTTPError) and \ 54 | not self.application.settings['debug']: 55 | # ie. a 500 error 56 | try: 57 | self._email_exception(exception) 58 | except: 59 | print "** Failing even to email exception **" 60 | 61 | if self.application.settings['debug']: 62 | # Because of 63 | # https://groups.google.com/d/msg/python-tornado/Zjv6_3OYaLs/CxkC7eLznv8J 64 | print "Exception!" 65 | print exception 66 | super(BaseHandler, self)._handle_request_exception(exception) 67 | 68 | 69 | def _email_exception(self, exception): # pragma: no cover 70 | import sys 71 | err_type, err_val, err_traceback = sys.exc_info() 72 | error = u'%s: %s' % (err_type, err_val) 73 | out = StringIO() 74 | subject = "%r on %s" % (err_val, self.request.path) 75 | print >> out, "TRACEBACK:" 76 | traceback.print_exception(err_type, err_val, err_traceback, 500, out) 77 | traceback_formatted = out.getvalue() 78 | print traceback_formatted 79 | print >> out, "\nREQUEST ARGUMENTS:" 80 | arguments = self.request.arguments 81 | if arguments.get('password') and arguments['password'][0]: 82 | password = arguments['password'][0] 83 | arguments['password'] = password[:2] + '*' * (len(password) - 2) 84 | pprint(arguments, out) 85 | 86 | print >> out, "\nCOOKIES:" 87 | for cookie in self.cookies: 88 | print >> out, " %s:" % cookie, 89 | print >> out, repr(self.get_secure_cookie(cookie)) 90 | 91 | print >> out, "\nREQUEST:" 92 | for key in ('full_url', 'protocol', 'query', 'remote_ip', 93 | 'request_time', 'uri', 'version'): 94 | print >> out, " %s:" % key, 95 | value = getattr(self.request, key) 96 | if callable(value): 97 | try: 98 | value = value() 99 | except: 100 | pass 101 | print >> out, repr(value) 102 | 103 | print >> out, "\nGIT REVISION: ", 104 | print >> out, self.application.settings['git_revision'] 105 | 106 | print >> out, "\nHEADERS:" 107 | pprint(dict(self.request.headers), out) 108 | 109 | send_email(self.application.settings['email_backend'], 110 | subject, 111 | out.getvalue(), 112 | self.application.settings['webmaster'], 113 | self.application.settings['admin_emails'], 114 | ) 115 | 116 | def get_current_user(self): 117 | # the 'user' cookie is for securely logged in people 118 | user_id = self.get_secure_cookie("user") 119 | if user_id: 120 | return self.db.User.one({'_id': ObjectId(user_id)}) 121 | 122 | def get_cdn_prefix(self): 123 | """return something that can be put in front of the static filename 124 | E.g. if filename is '/static/image.png' and you return '//cloudfront.com' 125 | then final URL presented in the template becomes 126 | '//cloudfront.com/static/image.png' 127 | """ 128 | return self.application.settings.get('cdn_prefix') 129 | 130 | def write_json(self, struct, javascript=False): 131 | self.set_header("Content-Type", "application/json; charset=UTF-8") 132 | self.write(tornado.escape.json_encode(struct)) 133 | 134 | def is_admin_user(self, user): 135 | return user.email in settings.ADMIN_EMAILS 136 | 137 | def get_base_options(self): 138 | # The templates rely on these variables 139 | options = dict(user=None, 140 | user_name=None, 141 | is_admin_user=False) 142 | 143 | # default settings 144 | settings = dict( 145 | disable_sound=False, 146 | ) 147 | 148 | user = self.get_current_user() 149 | user_name = None 150 | 151 | if user: 152 | if self.get_secure_cookie('user'): 153 | options['user'] = user 154 | options['is_admin_user'] = self.is_admin_user(user) 155 | if user.name: 156 | user_name = user.name 157 | elif user.email: 158 | user_name = user.email 159 | else: 160 | user_name = "stranger" 161 | options['user_name'] = user_name 162 | 163 | options['settings'] = settings 164 | options['git_revision'] = self.application.settings['git_revision'] 165 | options['debug'] = self.application.settings['debug'] 166 | return options 167 | 168 | 169 | @route('/', name="home") 170 | class HomeHandler(BaseHandler): 171 | 172 | def get(self, by=None, tags=None): 173 | options = self.get_base_options() 174 | user = options['user'] 175 | gists_search = {} 176 | comment_search = {} 177 | 178 | options['your_gists_count'] = 0 179 | options['your_gists'] = [] 180 | options['recent_comments_your_gists_count'] = 0 181 | options['recent_comments_your_gists'] = [] 182 | 183 | if by or tags: 184 | if by is not None: 185 | gists_search = {'user.$id': by._id} 186 | comment_search = {'user.$id': by._id} 187 | else: 188 | gists_search['tags'] = re.compile( 189 | '|'.join(re.escape(tag) for tag in tags), re.I) 190 | elif options['user']: 191 | _ids = [x._id for x in 192 | self.db.Gist.find({'user.$id': options['user']._id})] 193 | your_comment_search = {'gist.$id':{'$in':_ids}} 194 | options['recent_comments_your_gists_count'] = \ 195 | self.db.Comment.find(your_comment_search).count() 196 | options['recent_comments_your_gists'] = \ 197 | self.db.Comment.find(your_comment_search)\ 198 | .sort('add_date', DESCENDING).limit(20) 199 | 200 | options['your_gists_count'] = \ 201 | self.db.Gist.find({'user.$id': options['user']._id}).count() 202 | options['your_gists'] = \ 203 | self.db.Gist.find({'user.$id': options['user']._id})\ 204 | .sort('add_date', DESCENDING).limit(20) 205 | 206 | options['by'] = by 207 | options['count_gists'] = self.db.Gist.find(gists_search).count() 208 | options['gists'] = self.db.Gist.find(gists_search)\ 209 | .sort('add_date', DESCENDING) 210 | 211 | options['count_comments'] = self.db.Comment.find(comment_search).count() 212 | options['recent_comments'] = self.db.Comment.find(comment_search)\ 213 | .sort('add_date', DESCENDING).limit(20) 214 | 215 | options['gist_points'] = (self.db.GistPoints.find({'points': {'$gt':0}}) 216 | .sort('points', DESCENDING).limit(10)) 217 | self.render("home.html", **options) 218 | 219 | @route('/by/(\w+)$', name="by_user") 220 | class ByLoginHomeHandler(HomeHandler): 221 | 222 | def get(self, login): 223 | user = self.db.User.one({'login':login}) 224 | if not user: 225 | raise tornado.web.HTTPError(404, "No user by that login") 226 | super(ByLoginHomeHandler, self).get(by=user) 227 | 228 | @route('/tags/(.*)', name="by_tags") 229 | class ByTagsHomeHandler(HomeHandler): 230 | 231 | def get(self, tags_str): 232 | tags = [x.strip() for x in tags_str.split('/') if x.strip()] 233 | if not tags: 234 | raise tornado.web.HTTPError(404, "No valid tags") 235 | super(ByTagsHomeHandler, self).get(tags=tags) 236 | 237 | @route('/feeds/atom/latest/', name="feeds_atom_latest") 238 | class FeedsAtomLatestHandler(BaseHandler): 239 | 240 | def get(self): 241 | feed_url = self.request.full_url() 242 | base_url = 'http://%s' % self.request.host 243 | feed = pyatom.AtomFeed(title=settings.TITLE, 244 | subtitle=settings.SUB_TITLE, 245 | feed_url=feed_url, 246 | url=base_url, 247 | author=settings.DOMAIN_NAME) 248 | 249 | recently = datetime.datetime.now() - datetime.timedelta(seconds=10) 250 | gist_search = {'add_date': {'$lt':recently}} 251 | for gist in self.db.Gist.find(gist_search)\ 252 | .limit(20).sort('add_date', DESCENDING): 253 | content_type = "text" 254 | content = "" 255 | if gist.discussion: 256 | content = markdown.markdown(gist.discussion) 257 | content_type = "html" 258 | full_gist_url = 'http://%s%s' % \ 259 | (self.request.host, self.reverse_url('view_gist', gist.gist_id)) 260 | feed.add(title="%s: %s" % (gist.gist_id, gist.description), 261 | content=content, 262 | content_type=content_type, 263 | author=gist.user.name if gist.user.name else gist.user.login, 264 | url=full_gist_url, 265 | id=gist.gist_id, 266 | updated=gist.add_date, 267 | ) 268 | self.set_header("Content-Type", "application/atom+xml; charset=UTF-8") 269 | self.write(feed.to_string()) 270 | 271 | 272 | class BaseAuthHandler(BaseHandler): 273 | 274 | def get_next_url(self, default='/'): 275 | next = default 276 | if self.get_argument('next', None): 277 | next = self.get_argument('next') 278 | elif self.get_cookie('next', None): 279 | next = self.get_cookie('next') 280 | self.clear_cookie('next') 281 | return next 282 | 283 | class CredentialsError(Exception): 284 | pass 285 | 286 | 287 | class GithubMixin(tornado.auth.OAuth2Mixin): 288 | _OAUTH_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token?" 289 | _OAUTH_AUTHORIZE_URL = "https://github.com/login/oauth/authorize?" 290 | _OAUTH_NO_CALLBACKS = False 291 | 292 | def get_authenticated_user(self, redirect_uri, client_id, client_secret, 293 | code, callback, extra_fields=None): 294 | """handles login""" 295 | http = httpclient.AsyncHTTPClient() 296 | args = { 297 | "redirect_uri": redirect_uri, 298 | "code": code, 299 | "client_id": client_id, 300 | "client_secret": client_secret, 301 | } 302 | 303 | fields = set(['id', 'name', 'first_name', 'last_name', 304 | 'locale', 'picture', 'link']) 305 | if extra_fields: 306 | fields.update(extra_fields) 307 | 308 | url = self._oauth_request_token_url(**args) 309 | http.fetch(url, 310 | self.async_callback(self._on_access_token, redirect_uri, client_id, 311 | client_secret, callback, fields)) 312 | 313 | def _on_access_token(self, redirect_uri, client_id, client_secret, 314 | callback, fields, response): 315 | if response.error: 316 | logging.warning('GitHub auth error: %s' % response) 317 | callback(None) 318 | return 319 | 320 | session = { 321 | "access_token": cgi.parse_qs(response.body)["access_token"][-1], 322 | "expires": cgi.parse_qs(response.body).get("expires") 323 | } 324 | self.github_request( 325 | path="/user", 326 | callback=self.async_callback( 327 | self._on_get_user_info, callback, session, fields), 328 | access_token=session["access_token"], 329 | fields=",".join(fields) 330 | ) 331 | 332 | def _on_get_user_info(self, callback, session, fields, user): 333 | if user is None: 334 | callback(None) 335 | return 336 | fieldmap = dict(user) 337 | fieldmap.update({"access_token": session["access_token"], 338 | "session_expires": session.get("expires")}) 339 | callback(fieldmap) 340 | 341 | def github_request(self, path, callback, access_token=None, 342 | post_args=None, **args): 343 | """ 344 | """ 345 | url = "https://api.github.com" + path 346 | all_args = {} 347 | if access_token: 348 | all_args["access_token"] = access_token 349 | all_args.update(args) 350 | all_args.update(post_args or {}) 351 | if all_args: url += "?" + urllib.urlencode(all_args) 352 | callback = self.async_callback(self._on_github_request, callback) 353 | http = httpclient.AsyncHTTPClient() 354 | if post_args is not None: 355 | http.fetch(url, method="POST", body=urllib.urlencode(post_args), 356 | callback=callback) 357 | else: 358 | http.fetch(url, callback=callback) 359 | 360 | def _on_github_request(self, callback, response): 361 | if response.error: 362 | logging.warning("Error response %s fetching %s", response.error, 363 | response.request.url) 364 | callback(None) 365 | return 366 | callback(tornado.escape.json_decode(response.body)) 367 | 368 | 369 | @route('/auth/github/', name="github_login") 370 | class GithubLoginHandler(BaseAuthHandler, GithubMixin): 371 | 372 | @tornado.web.asynchronous 373 | def get(self): 374 | settings_ = settings.OAUTH_SETTINGS 375 | if self.get_argument("code", False): 376 | self.get_authenticated_user( 377 | redirect_uri=settings_['redirect_url'], 378 | client_id=settings_['client_id'], 379 | client_secret=settings_['client_secret'], 380 | code=self.get_argument("code"), 381 | callback=self.async_callback( 382 | self._on_login)) 383 | return 384 | 385 | self.authorize_redirect(redirect_uri=settings_['redirect_url'], 386 | client_id=settings_['client_id'], 387 | extra_params={})#"scope": "read_stream,offline_access"}) 388 | 389 | def _on_login(self, github_user): 390 | if not github_user.get('login'): 391 | return self.redirect('/?login_failed=true') 392 | 393 | #pprint(github_user) 394 | user = self.db.User.one({'login':unicode(github_user['login'])}) 395 | if user is None: 396 | user = self.db.User() 397 | user.login = unicode(github_user['login']) 398 | #print "CREATE NEW USER" 399 | for key in ('email', 'name', 'company', 'gravatar_id', 'access_token'): 400 | if key in github_user: 401 | value = github_user[key] 402 | if value is not None: 403 | setattr(user, key, unicode(value)) 404 | user.save() 405 | self.set_secure_cookie("user", str(user._id), expires_days=100) 406 | 407 | self.redirect('/') 408 | #logging.error(user) 409 | 410 | 411 | 412 | @route(r'/auth/logout/', name="logout") 413 | class AuthLogoutHandler(BaseAuthHandler): 414 | def get(self): 415 | self.clear_all_cookies() 416 | self.redirect(self.get_next_url()) 417 | 418 | #@route(r'/auth/fake/(.*)', name="fake_auth") 419 | #class FakeAuth(BaseAuthHandler): 420 | # def get(self, login): 421 | # assert self.application.settings['debug'] 422 | # #for x in self.db.User.find(): 423 | # # print repr(x.login) 424 | # user = self.db.User.one({'login':login}) 425 | # self.set_secure_cookie("user", str(user._id), expires_days=1) 426 | # #self.write('faked') 427 | # self.redirect('/') 428 | -------------------------------------------------------------------------------- /apps/main/indexes.py: -------------------------------------------------------------------------------- 1 | from pymongo import ASCENDING, DESCENDING 2 | from models import User 3 | from mongokit import Connection 4 | import settings 5 | con = Connection() 6 | con.register([User]) 7 | db = con[settings.DEFAULT_DATABASE_NAME] 8 | 9 | def run(): 10 | collection = db.User.collection 11 | collection.ensure_index('login', unique=True, ttl=3000) # default ttl=300 12 | yield 'login' 13 | test() 14 | 15 | def test(): 16 | curs = db.User.find({'login':'abc123'}).explain()['cursor'] 17 | assert 'BtreeCursor' in curs 18 | -------------------------------------------------------------------------------- /apps/main/migrations/000.example.py.txt: -------------------------------------------------------------------------------- 1 | # Note: because this file is run from another python doing execfile() you might 2 | # suffer some weird behaviour if you try to call functions. 3 | 4 | from apps.main.models import UserSettings 5 | from mongokit import Connection 6 | con = Connection() 7 | con.register([UserSettings]) 8 | 9 | 10 | collection = con.gkc.user_settings 11 | print "Fixing", collection.UserSettings.find({'newsletter_opt_out':{'$exists': False}}).count(), "objects" 12 | for each in collection.UserSettings.find({'newsletter_opt_out':{'$exists': False}}): 13 | each['newsletter_opt_out'] = False 14 | each.save() 15 | -------------------------------------------------------------------------------- /apps/main/migrations/001.basic_indexes.py: -------------------------------------------------------------------------------- 1 | # Note: because this file is run from another python doing execfile() you might 2 | # suffer some weird behaviour if you try to call functions. 3 | 4 | from pymongo import ASCENDING, DESCENDING 5 | from apps.main.models import User 6 | from mongokit import Connection 7 | import settings 8 | con = Connection() 9 | con.register([User]) 10 | db = con[settings.DEFAULT_DATABASE_NAME] 11 | 12 | collection = db.User.collection 13 | collection.ensure_index('login', unique=True, ttl=3000) # default ttl=300 14 | 15 | # Test 16 | curs = db.User.find({'login':'peterbe'}).explain()['cursor'] 17 | assert 'BtreeCursor' in curs 18 | -------------------------------------------------------------------------------- /apps/main/migrations/always_gists.py: -------------------------------------------------------------------------------- 1 | from apps.gists.models import Gist 2 | from mongokit import Connection 3 | con = Connection() 4 | con.register([Gist]) 5 | import settings 6 | 7 | db = con[settings.DEFAULT_DATABASE_NAME] 8 | 9 | for key in db.Gist.structure: 10 | search = {key: {'$exists':False}} 11 | count = db.Gist.find(search).count() 12 | if count: 13 | print "for key", repr(key) 14 | print "fixing", count, "objects" 15 | type_ = type(db.Gist.structure[key]) 16 | default_value = db.Gist.default_values.get(key, None) 17 | if default_value is None: 18 | if type_ is list: 19 | default_value = [] 20 | elif type_ is unicode: 21 | default_value = u'' 22 | else: 23 | raise ValueError("Too hard to guess default value for %r" % type_) 24 | 25 | for each in db.Gist.find(search): 26 | each[key] = default_value 27 | each.save() 28 | -------------------------------------------------------------------------------- /apps/main/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from mongokit import Document, ValidationError 3 | 4 | class BaseDocument(Document): 5 | structure = { 6 | 'add_date': datetime.datetime, 7 | 'modify_date': datetime.datetime, 8 | } 9 | 10 | default_values = { 11 | 'add_date': datetime.datetime.now, 12 | 'modify_date': datetime.datetime.now 13 | } 14 | use_autorefs = True 15 | use_dot_notation = True 16 | 17 | 18 | class User(BaseDocument): 19 | # modelled on the Github user account 20 | __collection__ = 'users' 21 | structure = { 22 | 'login': unicode, 23 | 'email': unicode, 24 | 'company': unicode, 25 | 'name': unicode, 26 | 'gravatar_id': unicode, 27 | 'access_token': unicode, 28 | } 29 | 30 | use_autorefs = True 31 | required_fields = ['login'] 32 | -------------------------------------------------------------------------------- /apps/main/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # These are here so you can do: 2 | # ./run_tests.sh --autoreload apps.main.tests 3 | from test_models import * 4 | from test_handlers import * 5 | -------------------------------------------------------------------------------- /apps/main/tests/base.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | from urllib import urlencode 4 | from cStringIO import StringIO 5 | 6 | from tornado.httpclient import HTTPRequest 7 | from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase 8 | 9 | import app 10 | from apps.main.models import User 11 | from utils.http_test_client import TestClient, HTTPClientMixin 12 | 13 | 14 | class BaseModelsTestCase(unittest.TestCase): 15 | _once = False 16 | def setUp(self): 17 | if not self._once: 18 | self._once = True 19 | from mongokit import Connection 20 | self.con = Connection() 21 | self.con.register([User]) 22 | self.db = self.con.test 23 | self._emptyCollections() 24 | 25 | def _emptyCollections(self): 26 | [self.db.drop_collection(x) for x 27 | in self.db.collection_names() 28 | if x not in ('system.indexes',)] 29 | 30 | def tearDown(self): 31 | self._emptyCollections() 32 | 33 | class BaseHTTPTestCase(AsyncHTTPTestCase, LogTrapTestCase, HTTPClientMixin): 34 | 35 | _once = False 36 | def setUp(self): 37 | super(BaseHTTPTestCase, self).setUp() 38 | if not self._once: 39 | self._once = True 40 | self._emptyCollections() 41 | 42 | self._app.settings['email_backend'] = 'utils.send_mail.backends.locmem.EmailBackend' 43 | self._app.settings['email_exceptions'] = False 44 | self.client = TestClient(self) 45 | 46 | def _emptyCollections(self): 47 | db = self.db 48 | [db.drop_collection(x) for x 49 | in db.collection_names() 50 | if x not in ('system.indexes',)] 51 | 52 | @property 53 | def db(self): 54 | return self._app.con[self._app.database_name] 55 | 56 | def get_db(self): 57 | return self.db 58 | #return self._app.con[self._app.database_name] 59 | 60 | def get_app(self): 61 | return app.Application(database_name='test', 62 | xsrf_cookies=False, 63 | optimize_static_content=False) 64 | 65 | def decode_cookie_value(self, key, cookie_value): 66 | try: 67 | return re.findall('%s=([\w=\|]+);' % key, cookie_value)[0] 68 | except IndexError: 69 | raise ValueError("couldn't find %r in %r" % (key, cookie_value)) 70 | 71 | def reverse_url(self, *args, **kwargs): 72 | return self._app.reverse_url(*args, **kwargs) 73 | -------------------------------------------------------------------------------- /apps/main/tests/mock_data.py: -------------------------------------------------------------------------------- 1 | MOCK_GITHUB_USERS = {'peterbe':{ 2 | u'access_token': 'xxxxxxxxxxxxxx', 3 | u'blog': u'www.peterbe.com', 4 | u'collaborators': 0, 5 | u'company': u'Mozilla', 6 | u'created_at': u'2008/09/29 07:09:24 -0700', 7 | u'disk_usage': 28348, 8 | u'email': u'peter@fry-it.com', 9 | u'followers_count': 21, 10 | u'following_count': 21, 11 | u'gravatar_id': u'yyyyyyyyyyyyyyyyyyyyyy', 12 | u'id':100000, 13 | u'location': u'London, United Kingdom', 14 | u'login': u'peterbe', 15 | u'name': u'Peter Bengtsson', 16 | u'owned_private_repo_count': 2, 17 | u'permission': None, 18 | u'plan': {u'collaborators': 1, 19 | u'name': u'micro', 20 | u'private_repos': 5, 21 | u'space': 614400}, 22 | u'private_gist_count': 0, 23 | u'public_gist_count': 9, 24 | u'public_repo_count': 23, 25 | u'session_expires': None, 26 | u'total_private_repo_count': 2, 27 | u'type': u'User'} 28 | } 29 | 30 | 31 | MOCK_GISTS = {'1234': 32 | {'gists': [{'comments': [], 33 | 'created_at': '2011/04/07 10:03:25 -0700', 34 | 'description': 'Starting tornado with preforking (multiple threads, one port)', 35 | 'files': ['gistfile1.py'], 36 | 'owner': 'peterbe', 37 | 'public': True, 38 | 'repo': '908208'}]}, 39 | } 40 | -------------------------------------------------------------------------------- /apps/main/tests/test_handlers.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from pprint import pprint 3 | from time import mktime 4 | import re 5 | import datetime 6 | import json 7 | 8 | from base import BaseHTTPTestCase 9 | import utils.send_mail as mail 10 | 11 | class HandlersTestCaseBase(BaseHTTPTestCase): 12 | 13 | def setUp(self): 14 | super(HandlersTestCaseBase, self).setUp() 15 | # override the otherwise convenient client.login() to one 16 | # tailored for oauth authentication testing 17 | from apps.main.handlers import GithubLoginHandler 18 | GithubLoginHandler.get_authenticated_user = \ 19 | mocked_get_authenticated_user 20 | self.client.login = self.github_oauth_login 21 | 22 | def github_oauth_login(self, login): 23 | response = self.client.get('/auth/github/', {'code':'OK-%s'% login}) 24 | self.assertEqual(response.code, 302) 25 | 26 | 27 | class HandlersTestCase(HandlersTestCaseBase): 28 | 29 | def test_homepage(self): 30 | response = self.client.get('/') 31 | self.assertTrue(self.reverse_url('github_login') in response.body) 32 | 33 | self.client.login('peterbe') 34 | response = self.client.get('/') 35 | self.assertTrue(self.reverse_url('logout') in response.body) 36 | response = self.client.get(self.reverse_url('logout')) 37 | self.assertEqual(response.code, 302) 38 | response = self.client.get('/') 39 | self.assertTrue(self.reverse_url('github_login') not in response.body) 40 | 41 | def test_latest_feeds_atom(self): 42 | response = self.client.get('/feeds/atom/latest/') 43 | self.assertEqual(response.code, 200) 44 | self.assertTrue('Peter' in response.body) 79 | 80 | gist2 = self.db.Gist() 81 | gist2.user = peter 82 | gist2.gist_id = 8888 83 | gist2.description = u'Artifically created long ago' 84 | gist2.save() 85 | gist2.add_date -= datetime.timedelta(hours=1) 86 | gist2.save() 87 | 88 | response = self.client.get('/feeds/atom/latest/') 89 | assert response.code == 200 90 | self.assertTrue(-1 < response.body.find('1234') <\ 91 | response.body.find('8888')) 92 | 93 | 94 | def test_github_oauth_login(self): 95 | assert not self.db.User.one({'login':u'peterbe'}) 96 | response = self.client.get('/auth/github/') 97 | self.assertEqual(response.code, 302) 98 | self.assertTrue(response.headers['Location'].startswith( 99 | 'https://github.com')) 100 | response = self.client.get('/auth/github/', {'code':'OK-peterbe'}) 101 | self.assertEqual(response.code, 302) 102 | 103 | self.assertTrue(self.db.User.one({'login':u'peterbe'})) 104 | 105 | response = self.client.get('/') 106 | self.assertTrue(self.reverse_url('logout') in response.body) 107 | self.assertTrue(mock_data.MOCK_GITHUB_USERS['peterbe']['name'] 108 | in response.body) 109 | 110 | def test_github_oauth_failing_login(self): 111 | response = self.client.get('/auth/github/', {'code':'xxx'}) 112 | self.assertEqual(response.code, 302) 113 | self.assertEqual(response.headers['Location'], 114 | '/?login_failed=true') 115 | 116 | def test_adding_gist(self): 117 | assert not self.db.Gist.find().count() 118 | import apps.gists.handlers as _h 119 | _before = _h.tornado.httpclient.AsyncHTTPClient 120 | _h.tornado.httpclient.AsyncHTTPClient = MockAsyncHTTPClient 121 | try: 122 | response = self.client.post('/add/', {}) 123 | self.assertEqual(response.code, 403) 124 | self.client.login('peterbe') 125 | response = self.client.post('/add/', {}) 126 | self.assertEqual(response.code, 400) # no gist id 127 | response = self.client.post('/add/', {'gist_id':'1234'}) 128 | self.assertEqual(response.code, 302) 129 | 130 | assert self.db.Gist.find().count() 131 | gist = self.db.Gist.one() 132 | edit_url = self.reverse_url('edit_gist', gist.gist_id) 133 | self.assertEqual(edit_url, response.headers['Location']) 134 | 135 | # if you try to add it again it'll redirect to the view url 136 | response = self.client.post('/add/', {'gist_id':'1234'}) 137 | self.assertEqual(response.code, 302) 138 | view_url = self.reverse_url('view_gist', gist.gist_id) 139 | self.assertEqual(view_url, response.headers['Location']) 140 | 141 | # try adding a failing gist that github doesn't recognize 142 | response = self.client.post('/add/', {'gist_id':'99999'}) 143 | self.assertEqual(response.code, 302) 144 | self.assertTrue(response.headers['Location'].startswith( 145 | self.reverse_url('gist_not_found'))) 146 | 147 | # poor man's follow redirect 148 | response = self.client.get(response.headers['Location']) 149 | assert response.code == 200 150 | self.assertTrue('Gist not found' in response.body) 151 | self.assertTrue('https://gist.github.com/99999' in response.body) 152 | 153 | finally: 154 | _h.tornado.httpclient.AsyncHTTPClient = _before 155 | 156 | def test_viewing_and_edit_gist(self): 157 | peter = self.db.User() 158 | peter.login = u'peterbe' 159 | peter.email = u'' 160 | peter.company = u'' 161 | peter.name = u'Peter' 162 | peter.save() 163 | 164 | gist = self.db.Gist() 165 | gist.user = peter 166 | gist.gist_id = 1234 167 | gist.created_at = u'yesterday' 168 | gist.files = [u'foo.py'] 169 | gist.description = u'Testing the Atom feed' 170 | gist.discussion = u"""Full `markdown` description: 171 | function foo(): 172 | return bar 173 | 174 | [peterbe](http://www.peterbe.com) 175 | """ 176 | gist.save() 177 | 178 | felinx = self.db.User() 179 | felinx.login = u'felinx' 180 | felinx.email = u'' 181 | felinx.company = u'ABC' 182 | felinx.name = u'Felinx' 183 | felinx.save() 184 | 185 | gist2 = self.db.Gist() 186 | gist2.user = felinx 187 | gist2.gist_id = 555 188 | gist2.created_at = u'yesterday' 189 | gist2.files = [u'bar.py'] 190 | gist2.description = u'Another gist' 191 | gist2.discussion = u"""discuss""" 192 | gist2.save() 193 | 194 | view_url = self.reverse_url('view_gist', 99999) 195 | response = self.client.get(view_url) 196 | self.assertEqual(response.code, 404) 197 | 198 | view_url = self.reverse_url('view_gist', 1234) 199 | response = self.client.get(view_url) 200 | self.assertEqual(response.code, 200) 201 | self.assertTrue('markdown' in response.body) 202 | self.assertTrue('foo.py' in response.body) 203 | self.assertTrue(self.reverse_url('by_user', 'peterbe') in response.body) 204 | self.assertTrue(gist.description in response.body) 205 | 206 | # log in as the owner of it 207 | self.client.login('peterbe') 208 | response = self.client.get(view_url) 209 | self.assertEqual(response.code, 200) 210 | edit_url = self.reverse_url('edit_gist', 1234) 211 | delete_url = self.reverse_url('delete_gist', 1234) 212 | self.assertTrue(edit_url in response.body) 213 | self.assertTrue(delete_url in response.body) 214 | 215 | other_edit_url = self.reverse_url('edit_gist', 555) 216 | response = self.client.get(other_edit_url) 217 | self.assertEqual(response.code, 403) 218 | other_delete_url = self.reverse_url('delete_gist', 555) 219 | response = self.client.get(other_delete_url) 220 | self.assertEqual(response.code, 403) 221 | 222 | response = self.client.get(edit_url) 223 | self.assertEqual(response.code, 200) 224 | self.assertTrue('%s' % gist.discussion in response.body) 225 | 226 | post_data = { 227 | 'discussion': 'Different', 228 | 'description': 'Short description', 229 | } 230 | response = self.client.post(other_edit_url, post_data) 231 | self.assertEqual(response.code, 403) 232 | 233 | response = self.client.post(edit_url, post_data) 234 | self.assertEqual(response.code, 302) 235 | response = self.client.get(view_url) 236 | self.assertEqual(response.code, 200) 237 | self.assertTrue('Short description' in response.body) 238 | self.assertTrue('Different' in response.body) 239 | 240 | response = self.client.get('/') 241 | self.assertEqual(response.code, 200) 242 | self.assertTrue(self.reverse_url('by_user', 'peterbe') in response.body) 243 | self.assertTrue(self.reverse_url('by_user', 'felinx') in response.body) 244 | 245 | response = self.client.get(self.reverse_url('by_user', 'unheardof')) 246 | self.assertEqual(response.code, 404) 247 | 248 | response = self.client.get(self.reverse_url('by_user', 'peterbe')) 249 | self.assertEqual(response.code, 200) 250 | self.assertTrue(self.reverse_url('view_gist', 1234) in response.body) 251 | self.assertTrue(self.reverse_url('view_gist', 555) not in response.body) 252 | self.assertTrue(self.reverse_url('by_user', 'felinx') not in response.body) 253 | 254 | 255 | self.assertEqual(self.db.Gist.find().count(), 2) 256 | response = self.client.get(delete_url) 257 | self.assertEqual(response.code, 302) 258 | self.assertEqual(self.db.Gist.find().count(), 1) 259 | 260 | import mock_data 261 | def mocked_get_authenticated_user(self, code, callback, **kwargs): 262 | if code.startswith('OK') and len(code.split('OK-')) == 2: 263 | callback(mock_data.MOCK_GITHUB_USERS[code.split('OK-')[1]]) 264 | else: 265 | callback({}) 266 | 267 | class _FakeBody(dict): 268 | def __getattr__(self, key): 269 | return self[key] 270 | 271 | 272 | class MockAsyncHTTPClient(object): 273 | def fetch(self, url, callback): 274 | gist_id = url.split('/')[-1] 275 | gist = mock_data.MOCK_GISTS.get(str(gist_id), {}) 276 | callback(_FakeBody({'body':json.dumps(gist)})) 277 | -------------------------------------------------------------------------------- /apps/main/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from mongokit import RequireFieldError, ValidationError 2 | import datetime 3 | #import sys; sys.path.insert(0, '..') 4 | from base import BaseModelsTestCase 5 | 6 | class ModelsTestCase(BaseModelsTestCase): 7 | 8 | def test_create_user(self): 9 | user = self.db.User() 10 | user.login = u'peterbe' 11 | assert user.add_date 12 | assert user.modify_date 13 | user.save() 14 | 15 | inst = self.db.users.User.one() 16 | assert inst.login 17 | inst.save() 18 | -------------------------------------------------------------------------------- /apps/main/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | class UtilsTestCase(unittest.TestCase): 5 | 6 | def test_parse_datetime(self): 7 | from utils import parse_datetime, DatetimeParseError 8 | 9 | r = parse_datetime('1285041600000') 10 | self.assertEqual(r.year, 2010) 11 | 12 | r = parse_datetime('1283140800') 13 | self.assertEqual(r.year, 2010) 14 | 15 | r = parse_datetime('1286744467.0') 16 | self.assertEqual(r.year, 2010) 17 | 18 | self.assertRaises(DatetimeParseError, parse_datetime, 'junk') 19 | 20 | 21 | def test_encrypt_password(self): 22 | from utils import encrypt_password 23 | 24 | p = encrypt_password('', log_rounds=1) 25 | p2 = encrypt_password('', log_rounds=1) 26 | self.assertNotEqual(p, p2) 27 | 28 | self.assertTrue(isinstance(p, unicode)) 29 | self.assertTrue('$bcrypt$' in p) 30 | 31 | # simulate what the User class's check_password does 32 | import bcrypt 33 | p = 'secret' 34 | r = encrypt_password(p, log_rounds=2) 35 | hashed = r.split('$bcrypt$')[-1].encode('utf8') 36 | self.assertEqual(hashed, bcrypt.hashpw(p, hashed)) 37 | 38 | def test_valid_email(self): 39 | from utils import valid_email 40 | self.assertTrue(valid_email('peterbe@gmail.com')) 41 | self.assertTrue(valid_email("peter'be@gmail.com")) 42 | 43 | self.assertTrue(not valid_email('peterbe @gmail.com')) 44 | self.assertTrue(not valid_email("peter'be@gmai")) 45 | 46 | def test_random_string(self): 47 | from utils import random_string 48 | 49 | x = random_string(10) 50 | self.assertEqual(len(x), 10) 51 | y = random_string(10) 52 | self.assertEqual(len(y), 10) 53 | self.assertNotEqual(x, y) 54 | -------------------------------------------------------------------------------- /apps/main/ui_modules.py: -------------------------------------------------------------------------------- 1 | import markdown 2 | import datetime 3 | import re 4 | from base64 import encodestring 5 | import stat 6 | import os 7 | import cPickle 8 | from time import time 9 | import marshal 10 | from tempfile import gettempdir 11 | import tornado.web 12 | import tornado.escape 13 | from utils.timesince import smartertimesince 14 | from utils import gravatar_html, html_quote 15 | from subprocess import Popen, PIPE 16 | from utils.truncate import truncate_words 17 | 18 | class Footer(tornado.web.UIModule): 19 | def render(self, user=None): 20 | return self.render_string("modules/footer.html", 21 | calendar_link=self.request.path != '/', 22 | user=user, 23 | ) 24 | 25 | class TruncateWords(tornado.web.UIModule): 26 | def render(self, string, max_words=20): 27 | return html_quote(truncate_words(string, max_words)) 28 | 29 | class TruncateString(tornado.web.UIModule): 30 | def render(self, string, max_length=30): 31 | if len(string) > max_length: 32 | return string[:max_length] + '...' 33 | return string 34 | 35 | 36 | class TimeSince(tornado.web.UIModule): 37 | def render(self, date, date2=None): 38 | return smartertimesince(date, date2) 39 | 40 | class RenderText(tornado.web.UIModule): 41 | def render(self, text, format='plaintext'): 42 | if format == 'markdown': 43 | return markdown.markdown(text, safe_mode="escape") 44 | else: 45 | # plaintext 46 | html = '

%s

' % tornado.escape.linkify(text).replace('\n','
\n') 47 | return html 48 | 49 | class ShowGravatar(tornado.web.UIModule): 50 | def render(self, gravatar_id, width_and_height=140): 51 | return gravatar_html(self.handler.is_secure(), gravatar_id, width_and_height) 52 | -------------------------------------------------------------------------------- /apps/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Tornado Gists - explaining Tornado on gist at a time{% end %} 7 | 8 | 9 | 10 | 11 | {% block extra_head %} 12 | {% end %} 13 | 14 | 15 |
16 | 35 | 36 |
37 | 38 |
39 | {% block main %} 40 | 41 | {% end %} 42 |
43 | 44 |
 
45 | 49 |
50 |
51 | 52 | 53 | {% block extra_js %} 54 | 55 | {% end %} 56 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /apps/templates/gist.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{ gist.description }} | Tornado Gists 5 | {% end %} 6 | 7 | {% block extra_head %} 8 | 25 | 26 | {% if edit %} 27 | 28 | {% end %} 29 | 30 | 31 | 46 | 47 | {% end %} 48 | 49 | {% block main %} 50 | 51 | 52 | 75 | 76 | {% if edit %} 77 |
{% raw xsrf_form_html() %} 78 | {% end %} 79 | 80 |

{{ gist.gist_id }}: 81 | {% if edit %} 82 | 83 | {% else %} 84 | {{ gist.description }} 85 | {% end %} 86 |

87 | 88 | {% if edit %} 89 |

(comma separated)

90 |
91 |

92 |
93 |

Text is rendered as Markdown

94 |

Cancel

95 |
96 | {% else %} 97 | {% if user and gist.user == user %} 98 |

Your gist, wanna delete it or 100 | edit it?

101 | {% else %} 102 |

Added 103 | {% module TimeSince(gist.add_date) %} ago 104 | by {{ gist.user.name if gist.user.name else gist.user.login }} 105 | {% if gist.user.gravatar_id %} 106 | {% module ShowGravatar(gist.user.gravatar_id, 30) %} 107 | {% end %} 108 |

109 | {% end %} 110 | 111 |

Love

112 |
{% raw xsrf_form_html() %} 113 | {{ vote_points }} 114 | you loved it! 115 | change your mind 116 | Add your love! 117 |
118 | {% if gist.tags %} 119 |

Tags

120 |
121 | {% for i, tag in enumerate(gist.tags) %} 122 | {{ tag }}{% if i + 1 < len(gist.tags) %}, {% end %} 123 | 124 | {% end %} 125 |
126 | {% end %} 127 | 128 | {% if gist.discussion %} 129 |

Discussion

130 |
{% module RenderText(gist.discussion, gist.discussion_format) %}
131 | {% end %} 132 | 133 |
134 | {% if gist.owner == gist.user.login %} 135 | {% else %} 136 |

Originally created 137 | {% if gist.owner %} 138 | by {{ gist.owner }} 139 | {% end %} 140 | {{ gist.created_at }} 141 |

142 | {% end %} 143 |
144 | {% end %} 145 | 146 | {% for i, filename in enumerate(gist.files) %} 147 |
148 |

File: {{ filename }}

149 | 150 | 153 | 154 | 157 | 160 | 161 | 162 |
163 | {% end %} 164 | 165 | 166 | 167 | {% end %} 168 | 169 | 170 | {% block extra_js %} 171 | {% if edit %} 172 | 173 | {% end %} 174 | 175 | 176 | {% end %} 177 | -------------------------------------------------------------------------------- /apps/templates/gist_not_found.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Gist not found :( | Tornado Gists 5 | {% end %} 6 | 7 | 8 | {% block main %} 9 | 10 |

Gist not found

11 | 12 | {% if gist_id %} 13 |

Sorry
14 | The gist you entered could not be found on github's API.
15 | Does it work here? 16 | https://gist.github.com/{{ gist_id }} 17 |
18 |
19 | Please check that you entered it correctly. Try again 20 |

21 | 22 | {% end %} 23 | 24 | 25 | {% end %} 26 | -------------------------------------------------------------------------------- /apps/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_head %} 4 | 7 | {% end %} 8 | 9 | {% block main %} 10 |
11 | {% raw xsrf_form_html() %} 12 | 13 | {% if not user %} 14 | 19 | {% end %} 20 | 21 |
22 | Github Gist ID: 23 | | Create a new gist on Github (then come back) 24 |
25 |
26 | 27 | {% if by %} 28 |

{{ by.name }}'s gists ({{ count_gists }})

29 |

clear search

30 | {% else %} 31 |

Gists ({{ count_gists }})

32 | {% end %} 33 | 34 | {% if your_gists_count %} 35 |

Your gists ({{ your_gists_count }})

36 | 48 | {% end %} 49 | 50 |

Recent gists

51 | 62 | 63 |

Most loved gists

64 |
    65 | {% for item in gist_points %} 66 |
  1. 67 | {{ item.points }} 68 | {{ item.gist.gist_id }}, 69 | {{ item.gist.description }} 70 |
  2. 71 | {% end %} 72 |
73 |

See all loved gists →

74 | 75 |

Comments ({{ count_comments }})

76 | 77 | {% if recent_comments_your_gists_count %} 78 |

Recent comments to your gists

79 | 86 | {% end %} 87 | 88 |

Recent comments

89 | 96 | 97 | 98 | {% end %} 99 | 100 | {% block extra_js %} 101 | {% if not user %} 102 | 113 | {% end %} 114 | {% end %} 115 | -------------------------------------------------------------------------------- /apps/templates/modules/footer.html: -------------------------------------------------------------------------------- 1 |

2 | © 3 | Home 4 | Questions 5 | About 6 | Help 7 |

8 | -------------------------------------------------------------------------------- /apps/templates/user/sharing.html: -------------------------------------------------------------------------------- 1 |

Sharing your calendar

2 | 3 | -------------------------------------------------------------------------------- /apps/templates/voting/most_loved.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %} 3 | Most loved | Tornado Gists 4 | {% end %} 5 | 6 | {% block main %} 7 |

Most loved gists

8 | 9 | 10 | {% for item in gist_points %} 11 | 12 | 13 | 14 | 15 | {% end %} 16 |
{{ item.gist.gist_id }}: {{ item.gist.description }}{{ item.points }}
17 | 18 |

Most loved users

19 | 20 | 21 | {% for item in user_points %} 22 | 23 | 24 | 25 | 26 | {% end %} 27 |
{{ item.user.login }}{{ item.points }}
28 | 29 | 30 | 31 | {% end %} 32 | -------------------------------------------------------------------------------- /apps/templates/voting/most_loved_user.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | {% block title %} 3 | Love for {{ user_points.user.login }}'s gists | Tornado Gists 4 | {% end %} 5 | 6 | {% block main %} 7 |
8 | {{ user_points.points }} 9 |
10 | 11 |

Love for {{ user_points.user.login }}'s gists

12 | 13 |

Total of {{ user_points.points }} points across {{ no_total_gists }} uploaded gists.

14 | 15 |

Gists by {{ user_points.user.login }} most loved

16 | 17 | 18 | {% for item in gist_points %} 19 | 20 | 21 | 22 | 23 | {% end %} 24 |
{{ item.gist.gist_id }}: {{ item.gist.description }}{{ item.points }}
25 | 26 | 27 |

← Go back to Most loved

28 | 29 | {% end %} 30 | -------------------------------------------------------------------------------- /apps/voting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/apps/voting/__init__.py -------------------------------------------------------------------------------- /apps/voting/handlers.py: -------------------------------------------------------------------------------- 1 | from pymongo import ASCENDING, DESCENDING 2 | from urllib import quote 3 | from apps.main.handlers import BaseHandler 4 | from apps.gists.handlers import GistBaseHandler 5 | from utils.routes import route, route_redirect 6 | from models import Vote 7 | 8 | @route(r'/(\d+)/vote/toggle/$', name="vote_toggle_gist") 9 | class VoteupGistHandler(GistBaseHandler): 10 | 11 | def post(self, gist_id): 12 | # used by $.post() 13 | user = self.get_current_user() 14 | if not user: 15 | self.write_json(dict(not_logged_in=True)) 16 | return 17 | gist = self.find_gist(gist_id) 18 | search = {'user.$id': user._id, 19 | 'gist.$id': gist._id, 20 | 'comment': None} 21 | vote = self.db.Vote.one(search) 22 | if not vote: 23 | vote = self.db.Vote() 24 | vote.user = user 25 | vote.gist = gist 26 | vote.points = 1 27 | vote.save() 28 | points_diff = 1 29 | else: 30 | vote.delete() 31 | points_diff = -1 32 | 33 | # always return the total 34 | del search['user.$id'] 35 | points = sum([x['points'] for x in 36 | self.db.Vote.collection.find(search)]) 37 | # to be able to do quick aggregates, on summed votes, 38 | # copy this number over to the gist now 39 | gist_points = self.db.GistPoints.one({'gist.$id': gist._id}) 40 | if not gist_points: 41 | gist_points = self.db.GistPoints() 42 | gist_points.gist = gist 43 | gist_points.points = points 44 | gist_points.save() 45 | 46 | user_points = self.db.UserPoints.one({'user.$id': gist.user._id}) 47 | if not user_points: 48 | user_points = self.db.UserPoints() 49 | user_points.user = gist.user 50 | user_points.points = 0 51 | user_points.points += points_diff 52 | user_points.save() 53 | 54 | self.write_json(dict(points=points)) 55 | 56 | 57 | @route(r'/most/loved/', name="most_loved") 58 | class MostLovedHandler(BaseHandler): 59 | def get(self): 60 | options = self.get_base_options() 61 | options['page_title'] = "Most loved" 62 | options['gist_points'] = (self.db.GistPoints 63 | .find({'points': {'$gt': 0}}) 64 | .sort('points', DESCENDING).limit(20)) 65 | options['user_points'] = (self.db.UserPoints 66 | .find({'points': {'$gt': 0}}) 67 | .sort('points', DESCENDING).limit(20)) 68 | 69 | self.render("voting/most_loved.html", **options) 70 | 71 | @route(r'/most/loved/(\w+)', name="most_loved_user") 72 | class MostLovedUserHandler(BaseHandler): 73 | 74 | def get(self, login): 75 | user = self.db.User.one({'login':login}) 76 | if not user: 77 | raise tornado.web.HTTPError(404, "No user by that login") 78 | 79 | user_points = self.db.UserPoints.one({'user.$id': user._id}) 80 | if not user_points: 81 | self.write("Sorry, no points at all for %s yet" % user.login) 82 | return 83 | options = self.get_base_options() 84 | options['user_points'] = user_points 85 | gist_ids = [x['_id'] for x in self.db.Gist.collection.find({'user.$id': user._id})] 86 | options['gist_points'] = (self.db.GistPoints 87 | .find({'points': {'$gt': 0}, 88 | 'gist.$id': {'$in': gist_ids}}) 89 | .sort('points', DESCENDING).limit(20)) 90 | options['no_total_gists'] = self.db.Gist.find({'user.$id': user._id}).count() 91 | 92 | self.render('voting/most_loved_user.html', **options) 93 | -------------------------------------------------------------------------------- /apps/voting/indexes.py: -------------------------------------------------------------------------------- 1 | from pymongo import ASCENDING, DESCENDING 2 | from models import Vote, GistPoints, UserPoints 3 | from mongokit import Connection 4 | import settings 5 | con = Connection() 6 | con.register([Vote, GistPoints, UserPoints]) 7 | db = con[settings.DEFAULT_DATABASE_NAME] 8 | 9 | def run(): 10 | collection = db.Vote.collection 11 | collection.ensure_index('user.$id') 12 | yield 'user.$id' 13 | collection.ensure_index('gist.$id') 14 | yield 'user.$id' 15 | 16 | collection = db.GistPoints.collection 17 | collection.ensure_index('gist.$id') 18 | yield 'user.$id' 19 | collection.ensure_index([('points',DESCENDING)]) 20 | yield 'points' 21 | 22 | collection = db.UserPoints.collection 23 | collection.ensure_index('user.$id') 24 | yield 'user.$id' 25 | collection.ensure_index([('points',DESCENDING)]) 26 | yield 'points' 27 | 28 | test() 29 | 30 | 31 | def test(): 32 | any_obj_id = list(db.Vote.find().limit(1))[0]._id 33 | curs = db.Vote.find({'user.$id':any_obj_id}).explain()['cursor'] 34 | assert 'BtreeCursor' in curs 35 | curs = db.Vote.find({'gist.$id':any_obj_id}).explain()['cursor'] 36 | assert 'BtreeCursor' in curs 37 | 38 | curs = db.GistPoints.find({'gist.$id':any_obj_id}).explain()['cursor'] 39 | assert 'BtreeCursor' in curs 40 | curs = db.GistPoints.find().sort('points', DESCENDING).explain()['cursor'] 41 | assert 'BtreeCursor' in curs 42 | curs = db.UserPoints.find({'user.$id':any_obj_id}).explain()['cursor'] 43 | assert 'BtreeCursor' in curs 44 | curs = db.UserPoints.find().sort('points', DESCENDING).explain()['cursor'] 45 | assert 'BtreeCursor' in curs 46 | -------------------------------------------------------------------------------- /apps/voting/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pymongo.objectid import ObjectId 3 | from apps.main.models import BaseDocument, User 4 | from apps.gists.models import Gist, Comment 5 | 6 | class Vote(BaseDocument): 7 | __collection__ = 'votes' 8 | structure = { 9 | 'user': User, 10 | 'gist': Gist, 11 | 'comment': Comment, 12 | 'points': int 13 | } 14 | 15 | default_values = { 16 | 'points': 1, 17 | } 18 | 19 | class GistPoints(BaseDocument): 20 | __collection__ = 'gist_voting_points' 21 | structure = { 22 | 'gist': Gist, 23 | 'points': int, 24 | } 25 | default_values = { 26 | 'points': 0 27 | } 28 | 29 | class UserPoints(BaseDocument): 30 | __collection__ = 'user_voting_points' 31 | structure = { 32 | 'user': User, 33 | 'points': int, 34 | } 35 | default_values = { 36 | 'points': 0 37 | } 38 | -------------------------------------------------------------------------------- /apps/voting/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # These are here so you can do: 2 | # ./run_tests.sh --autoreload apps.voting.tests 3 | from test_handlers import * 4 | -------------------------------------------------------------------------------- /apps/voting/tests/test_handlers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from apps.main.tests.test_handlers import HandlersTestCaseBase 3 | 4 | class VotingHandlersTestCase(HandlersTestCaseBase): 5 | 6 | def test_toggle_vote(self): 7 | peter = self.db.User() 8 | peter.login = u'peterbe' 9 | peter.email = u'' 10 | peter.company = u'' 11 | peter.name = u'Peter' 12 | peter.save() 13 | 14 | ernst = self.db.User() 15 | ernst.login = u'ernst' 16 | ernst.email = u'' 17 | ernst.company = u'' 18 | ernst.name = u'Ernst' 19 | ernst.save() 20 | 21 | gist = self.db.Gist() 22 | gist.user = ernst 23 | gist.gist_id = 1234 24 | gist.created_at = u'yesterday' 25 | gist.files = [u'foo.py'] 26 | gist.description = u'Testing the Atom feed' 27 | gist.discussion = u"""Full `markdown` description: 28 | function foo(): 29 | return bar 30 | """ 31 | gist.save() 32 | 33 | url = self.reverse_url('vote_toggle_gist', gist.gist_id) 34 | response = self.client.get(url) 35 | self.assertEqual(response.code, 405) 36 | response = self.client.post(url, {}) 37 | self.assertEqual(response.code, 200) 38 | self.assertTrue('not_logged_in' in response.body) 39 | 40 | self.client.login('peterbe') 41 | response = self.client.post(url, {}) 42 | self.assertEqual(response.code, 200) 43 | struct = json.loads(response.body) 44 | self.assertEqual(struct, {'points': 1}) 45 | 46 | vote = self.db.Vote.one() 47 | assert vote 48 | self.assertEqual(vote.gist._id, gist._id) 49 | self.assertEqual(vote.user._id, peter._id) 50 | 51 | gist_points = self.db.GistPoints.one({'gist.$id': gist._id}) 52 | self.assertEqual(gist_points.points, 1) 53 | 54 | assert not self.db.UserPoints.one({'user.$id': peter._id}) 55 | 56 | user_points = self.db.UserPoints.one({'user.$id': ernst._id}) 57 | assert user_points 58 | self.assertEqual(user_points.points, 1) 59 | 60 | # change your mind 61 | response = self.client.post(url, {}) 62 | self.assertEqual(response.code, 200) 63 | struct = json.loads(response.body) 64 | self.assertEqual(struct, {'points': 0}) 65 | 66 | gist_points = self.db.GistPoints.one({'gist.$id': gist._id}) 67 | self.assertEqual(gist_points.points, 0) 68 | 69 | assert not self.db.UserPoints.one({'user.$id': peter._id}) 70 | 71 | user_points = self.db.UserPoints.one({'user.$id': ernst._id}) 72 | assert user_points 73 | self.assertEqual(user_points.points, 0) 74 | 75 | # change your mind again 76 | response = self.client.post(url, {}) 77 | self.assertEqual(response.code, 200) 78 | struct = json.loads(response.body) 79 | self.assertEqual(struct, {'points': 1}) 80 | 81 | gist_points = self.db.GistPoints.one() 82 | 83 | # log in as the author of the gist and upvote your own 84 | import apps.main.tests.mock_data 85 | apps.main.tests.mock_data.MOCK_GITHUB_USERS['ernst'] = { 86 | 'name': 'Ernst Hemingway', 87 | 'login': 'ernst', 88 | } 89 | self.client.login('ernst') 90 | assert 'Hemingway' in self.client.get('/').body 91 | 92 | response = self.client.post(url, {}) 93 | self.assertEqual(response.code, 200) 94 | struct = json.loads(response.body) 95 | self.assertEqual(struct, {'points': 2}) 96 | 97 | def test_most_loved(self): 98 | peter = self.db.User() 99 | peter.login = u'peterbe' 100 | peter.email = u'' 101 | peter.company = u'' 102 | peter.name = u'Peter' 103 | peter.save() 104 | 105 | ernst = self.db.User() 106 | ernst.login = u'ernst' 107 | ernst.email = u'' 108 | ernst.company = u'' 109 | ernst.name = u'Ernst' 110 | ernst.save() 111 | 112 | gist = self.db.Gist() 113 | gist.user = peter 114 | gist.gist_id = 111 115 | gist.created_at = u'yesterday' 116 | gist.files = [u'foo.py'] 117 | gist.description = u'Testing the Atom feed' 118 | gist.discussion = u"""Full `markdown` description: 119 | function foo(): 120 | return bar 121 | """ 122 | gist.save() 123 | 124 | gist2 = self.db.Gist() 125 | gist2.user = ernst 126 | gist2.gist_id = 2222 127 | gist2.created_at = u'yesterday' 128 | gist2.files = [u'foo.py'] 129 | gist2.description = u'Foo 2' 130 | gist2.discussion = u"Stuff" 131 | gist2.save() 132 | 133 | # suppose both people voted for gist2 134 | vote = self.db.Vote() 135 | vote.user = peter 136 | vote.gist = gist2 137 | vote.points = 1 138 | vote.save() 139 | vote = self.db.Vote() 140 | vote.user = ernst 141 | vote.gist = gist2 142 | vote.points = 1 143 | vote.save() 144 | 145 | 146 | gp = self.db.GistPoints() 147 | gp.gist = gist2 148 | gp.points = 2 149 | gp.save() 150 | 151 | up = self.db.UserPoints() 152 | up.user = ernst 153 | up.points = 2 154 | up.save() 155 | 156 | url = self.reverse_url('most_loved') 157 | response = self.client.get(url) 158 | self.assertEqual(response.code, 200) 159 | self.assertTrue(gist2.description in response.body) 160 | self.assertTrue('>2<' in response.body) 161 | 162 | self.assertTrue(gist.description not in response.body) 163 | 164 | ernst_url = self.reverse_url('most_loved_user', ernst.login) 165 | self.assertTrue(ernst_url in response.body) 166 | 167 | # and now a vote for gist1 168 | vote = self.db.Vote() 169 | vote.user = ernst 170 | vote.gist = gist 171 | vote.points = 1 172 | vote.save() 173 | 174 | gp = self.db.GistPoints() 175 | gp.gist = gist 176 | gp.points = 1 177 | gp.save() 178 | 179 | up = self.db.UserPoints() 180 | up.user = peter 181 | up.points = 1 182 | up.save() 183 | 184 | response = self.client.get(url) 185 | self.assertEqual(response.code, 200) 186 | self.assertTrue(gist.description in response.body) 187 | self.assertTrue('>1<' in response.body) 188 | 189 | response = self.client.get(ernst_url) 190 | self.assertEqual(response.code, 200) 191 | self.assertTrue('2 points' in response.body) 192 | -------------------------------------------------------------------------------- /bin/_run_coverage_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import coverage 4 | 5 | from _run_tests import TEST_MODULES 6 | 7 | COVERAGE_MODULES = [ 8 | 'app', 9 | 'apps.main.models', 10 | 'apps.main.handlers', 11 | 'apps.gists.models', 12 | 'apps.gists.handlers', 13 | 'apps.voting.handlers', 14 | ] 15 | 16 | def all(): 17 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) 18 | 19 | if __name__ == '__main__': 20 | import tornado.testing 21 | 22 | cov = coverage.coverage() 23 | cov.use_cache(0) # Do not cache any of the coverage.py stuff 24 | cov.start() 25 | 26 | try: 27 | tornado.testing.main() 28 | except SystemExit, e: 29 | if e.code: 30 | # go ahead and raise the exit :( 31 | raise 32 | 33 | cov.stop() 34 | print '' 35 | print '----------------------------------------------------------------------' 36 | print ' Unit Test Code Coverage Results' 37 | print '----------------------------------------------------------------------' 38 | 39 | # Report code coverage metrics 40 | coverage_modules = [] 41 | for module in COVERAGE_MODULES: 42 | coverage_modules.append(__import__(module, globals(), locals(), [''])) 43 | cov.report(coverage_modules, show_missing=1) 44 | cov.html_report(coverage_modules, directory='coverage_report') 45 | # Print code metrics footer 46 | print '----------------------------------------------------------------------' 47 | -------------------------------------------------------------------------------- /bin/_run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os, sys 3 | if os.path.abspath(os.curdir) not in sys.path: 4 | sys.path.insert(0, os.path.abspath(os.curdir)) 5 | 6 | import unittest 7 | 8 | TEST_MODULES = [ 9 | 'apps.main.tests.test_handlers', 10 | 'apps.main.tests.test_models', 11 | 'apps.gists.tests.test_handlers', 12 | 'apps.gists.tests.test_models', 13 | 'apps.voting.tests.test_handlers', 14 | ] 15 | 16 | def all(): 17 | try: 18 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) 19 | except AttributeError, e: 20 | if "'module' object has no attribute 'test_handlers'" in str(e): 21 | # most likely because of an import error 22 | for m in TEST_MODULES: 23 | __import__(m, globals(), locals()) 24 | raise 25 | 26 | 27 | if __name__ == '__main__': 28 | import tornado.testing 29 | #import cProfile, pstats 30 | #cProfile.run('tornado.testing.main()') 31 | try: 32 | tornado.testing.main() 33 | except KeyboardInterrupt: 34 | pass # exit 35 | -------------------------------------------------------------------------------- /bin/ensure_indexes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os, sys 3 | if os.path.abspath(os.curdir) not in sys.path: 4 | sys.path.insert(0, os.path.abspath(os.curdir)) 5 | 6 | def main(*apps): 7 | if not apps: 8 | apps = [x for x in os.listdir('apps') 9 | if (os.path.isdir(os.path.join('apps', x)) and 10 | os.path.isfile(os.path.join('apps', x, 'indexes.py')))] 11 | 12 | for app in apps: 13 | _indexes = __import__('apps.%s' % app, globals(), locals(), ['indexes'], -1) 14 | runner = _indexes.indexes.run 15 | print app 16 | print '\t', ', '.join(runner()) 17 | 18 | return 0 19 | 20 | if __name__ == '__main__': 21 | import sys 22 | sys.exit(main(*sys.argv[1:])) 23 | -------------------------------------------------------------------------------- /bin/recalculate_voting_points.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, sys 4 | if os.path.abspath(os.curdir) not in sys.path: 5 | sys.path.insert(0, os.path.abspath(os.curdir)) 6 | from pymongo import ASCENDING, DESCENDING 7 | from apps.voting.models import Vote, GistPoints, UserPoints 8 | from apps.main.models import User 9 | from apps.gists.models import Gist, Comment 10 | from mongokit import Connection 11 | import settings 12 | 13 | def main(*args): 14 | con = Connection() 15 | con.register([Vote, GistPoints, UserPoints, 16 | User, Gist, Comment]) 17 | db = con[settings.DEFAULT_DATABASE_NAME] 18 | 19 | for user in db.User.find(): 20 | 21 | up = db.UserPoints.one({'user.$id': user._id}) 22 | total_up = 0 23 | if not up: 24 | up = db.UserPoints() 25 | up.points = 0 26 | up.user = user 27 | 28 | for gist in db.Gist.find({'user.$id': user._id}): 29 | gp = db.GistPoints.one({'gist.$id': gist._id}) 30 | if not gp: 31 | gp = db.GistPoints() 32 | gp.gist = gist 33 | gp.points = 0 34 | gp.points = sum(x['points'] for x in 35 | db.Vote.collection.find({'gist.$id': gist._id})) 36 | gp.save() 37 | total_up += gp.points 38 | up.points = total_up 39 | up.save() 40 | 41 | return 0 42 | 43 | if __name__ == '__main__': 44 | sys.exit(main(*sys.argv[1:])) 45 | -------------------------------------------------------------------------------- /bin/run_coverage_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python bin/_run_coverage_tests.py 4 | if [ "$?" == 0 ]; then 5 | open coverage_report/index.html 6 | fi 7 | -------------------------------------------------------------------------------- /bin/run_development_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./app.py --debug --dont_combine --logging=debug 3 | -------------------------------------------------------------------------------- /bin/run_migrations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import datetime 3 | import re 4 | from glob import glob 5 | 6 | import os, sys 7 | if os.path.abspath(os.curdir) not in sys.path: 8 | sys.path.insert(0, os.path.abspath(os.curdir)) 9 | 10 | __version__ = '1.1' 11 | 12 | def main(locations, patterns): 13 | def _filter(filename): 14 | if filename.endswith('.done') and not filename.endswith('.py'): 15 | return False 16 | if not (re.findall('^\d+', os.path.basename(filename)) or \ 17 | re.findall(r'^always', os.path.basename(filename))): 18 | return False 19 | if os.path.basename(filename).startswith('_'): 20 | return False 21 | if os.path.isfile(filename + '.done'): 22 | return False 23 | return True 24 | def _repr(filename): 25 | if os.path.basename(filename).startswith('always'): 26 | return (999, filename) # so it's run last 27 | return (int(re.findall('^(\d+)', os.path.basename(filename))[0]), 28 | filename) 29 | filenames = [] 30 | for location in locations: 31 | for pattern in patterns: 32 | filenames.extend([_repr(x) for x in glob(os.path.join(location, pattern)) 33 | if _filter(x)]) 34 | filenames.sort() 35 | for __, filename in filenames: 36 | sys.path.insert(0, os.path.abspath('.')) 37 | execfile(filename, globals(), locals()) 38 | t = datetime.datetime.now() 39 | t = t.strftime('%Y/%m/%d %H:%M:%S') 40 | if not os.path.basename(filename).startswith('always'): 41 | done_filename = filename + '.done' 42 | open(done_filename, 'w').write("%s\n" % t) 43 | print done_filename 44 | 45 | from settings import APPS 46 | locations = [os.path.join('apps', x, 'migrations') 47 | for x in APPS 48 | if os.path.isdir(os.path.join('apps', x, 'migrations'))] 49 | 50 | def run(*args): 51 | if not args: 52 | patterns = ['*.py'] 53 | else: 54 | patterns = args 55 | main(locations, patterns) 56 | return 0 57 | 58 | if __name__ == '__main__': 59 | import sys 60 | sys.exit(run(*sys.argv[1:])) -------------------------------------------------------------------------------- /bin/run_pyflakes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pyflakes.checker 3 | import compiler, sys 4 | import os 5 | 6 | def check(codeString, filename): 7 | """ 8 | Check the Python source given by C{codeString} for flakes. 9 | 10 | @param codeString: The Python source to check. 11 | @type codeString: C{str} 12 | 13 | @param filename: The name of the file the source came from, used to report 14 | errors. 15 | @type filename: C{str} 16 | 17 | @return: The number of warnings emitted. 18 | @rtype: C{int} 19 | """ 20 | # Since compiler.parse does not reliably report syntax errors, use the 21 | # built in compiler first to detect those. 22 | try: 23 | try: 24 | compile(codeString, filename, "exec") 25 | except MemoryError: 26 | # Python 2.4 will raise MemoryError if the source can't be 27 | # decoded. 28 | if sys.version_info[:2] == (2, 4): 29 | raise SyntaxError(None) 30 | raise 31 | except (SyntaxError, IndentationError), value: 32 | msg = value.args[0] 33 | 34 | (lineno, offset, text) = value.lineno, value.offset, value.text 35 | 36 | # If there's an encoding problem with the file, the text is None. 37 | if text is None: 38 | # Avoid using msg, since for the only known case, it contains a 39 | # bogus message that claims the encoding the file declared was 40 | # unknown. 41 | return ["%s: problem decoding source" % (filename, )] 42 | else: 43 | line = text.splitlines()[-1] 44 | 45 | if offset is not None: 46 | offset = offset - (len(text) - len(line)) 47 | 48 | return ['%s:%d: %s' % (filename, lineno, msg)] 49 | else: 50 | # Okay, it's syntactically valid. Now parse it into an ast and check 51 | # it. 52 | tree = compiler.parse(codeString) 53 | w = pyflakes.checker.Checker(tree, filename) 54 | 55 | lines = codeString.split('\n') 56 | messages = [message for message in w.messages 57 | if lines[message.lineno-1].find('pyflakes:ignore') < 0] 58 | messages.sort(lambda a, b: cmp(a.lineno, b.lineno)) 59 | 60 | return messages 61 | 62 | 63 | def checkPath(filename): 64 | """ 65 | Check the given path, printing out any warnings detected. 66 | 67 | @return: the number of warnings printed 68 | """ 69 | try: 70 | return check(file(filename, 'U').read() + '\n', filename) 71 | except IOError, msg: 72 | return ["%s: %s" % (filename, msg.args[1])] 73 | 74 | def checkPaths(filenames): 75 | warnings = [] 76 | for arg in filenames: 77 | if os.path.isdir(arg): 78 | for dirpath, dirnames, filenames in os.walk(arg): 79 | for filename in filenames: 80 | if filename.endswith('.py'): 81 | warnings.extend(checkPath(os.path.join(dirpath, filename))) 82 | else: 83 | warnings.extend(checkPath(arg)) 84 | return warnings 85 | 86 | import settings 87 | 88 | def run(*filenames): 89 | if not filenames: 90 | filenames = getattr(settings, 'PYFLAKES_DEFAULT_ARGS', ['apps']) 91 | warnings = checkPaths(filenames) 92 | for warning in warnings: 93 | print warning 94 | if warnings: 95 | print 'Total warnings: %d' % len(warnings) 96 | return 0 97 | 98 | if __name__ == '__main__': 99 | import sys 100 | sys.exit(run(*sys.argv[1:])) -------------------------------------------------------------------------------- /bin/run_shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import code, re 4 | import os, sys 5 | if os.path.abspath(os.curdir) not in sys.path: 6 | sys.path.insert(0, os.path.abspath(os.curdir)) 7 | 8 | if __name__ == '__main__': 9 | 10 | from apps.main.models import * 11 | from apps.main import models 12 | 13 | from apps.gists.models import * 14 | from mongokit import Connection, Document as mongokit_Document 15 | from pymongo.objectid import InvalidId, ObjectId 16 | con = Connection() 17 | 18 | import settings 19 | model_classes = [] 20 | for app_name in settings.APPS: 21 | _models = __import__('apps.%s' % app_name, globals(), locals(), 22 | ['models'], -1) 23 | try: 24 | models = _models.models 25 | except AttributeError: 26 | # this app simply doesn't have a models.py file 27 | continue 28 | for name in [x for x in dir(models) if re.findall('[A-Z]\w+', x)]: 29 | thing = getattr(models, name) 30 | try: 31 | if issubclass(thing, mongokit_Document): 32 | model_classes.append(thing) 33 | except TypeError: 34 | pass 35 | 36 | con.register(model_classes) 37 | 38 | db = con[settings.DEFAULT_DATABASE_NAME] 39 | print "AVAILABLE:" 40 | print '\n'.join(['\t%s'%x for x in locals().keys() 41 | if re.findall('[A-Z]\w+|db|con', x)]) 42 | print "Database available as 'db'" 43 | code.interact(local=locals()) 44 | -------------------------------------------------------------------------------- /bin/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python bin/_run_tests.py $@ 4 | -------------------------------------------------------------------------------- /bin/update-user-details.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import datetime 3 | from urllib import urlopen 4 | import json 5 | 6 | def pull_details(username): 7 | url = "http://github.com/api/v2/json/user/show/%s" % username 8 | content = urlopen(url).read() 9 | try: 10 | return json.loads(content)['user'] 11 | except KeyError: 12 | return None 13 | 14 | 15 | def run(max_updates=5): 16 | from apps.main.models import User 17 | from mongokit import Connection 18 | con = Connection() 19 | con.register([User]) 20 | import settings 21 | db = con[settings.DEFAULT_DATABASE_NAME] 22 | print db.User.find().count(), "users in total" 23 | today = datetime.date.today() 24 | today = datetime.datetime(*(today.timetuple()[:6])) 25 | search = {'modify_date': {'$lt': today}} 26 | print db.User.find(search).count(), "left to update today" 27 | for user in db.User.find(search).sort('modify_date', 1).limit(max_updates): 28 | print repr(user.login), user.modify_date 29 | details = pull_details(user.login) 30 | if details is None: 31 | print "FAIL!" 32 | print "??? http://github.com/api/v2/json/user/show/%s" % user.login 33 | continue 34 | for key in 'name company email gravatar_id'.split(): 35 | if key in details: 36 | if getattr(user, key, '') != details[key]: 37 | print "\t", key, repr(getattr(user, key, '*blank*')), 38 | print '-->', repr(details[key]) 39 | setattr(user, key, details[key]) 40 | user.modify_date = datetime.datetime.now() 41 | user.save() 42 | 43 | def main(*args): 44 | max_updates = 100 45 | if args and args[0].isdigit(): 46 | max_updates = int(args[0]) 47 | run(max_updates=max_updates) 48 | return 0 49 | 50 | if __name__ == '__main__': 51 | import sys 52 | sys.exit(main(*sys.argv[1:])) 53 | -------------------------------------------------------------------------------- /external_apps.txt: -------------------------------------------------------------------------------- 1 | mongokit 2 | python-dateutil 3 | markdown 4 | -------------------------------------------------------------------------------- /find_console.log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | gg console.log | grep -v apply | grep -v '/ext/' 3 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | _ROOT = os.path.dirname(__file__) 4 | 5 | TITLE = u"Tornado Gists" 6 | SUB_TITLE = "explaining Tornado on gist at a time" 7 | DOMAIN_NAME = "TornadoGists.org" 8 | APPS = ( 9 | 'main', 10 | 'gists', 11 | 'voting', 12 | ) 13 | 14 | LOGIN_URL = "/auth/login/" 15 | COOKIE_SECRET = "Y2o1TzK2YQAGEYdkL5gmueJIFuY37EQm92XsTo1v/Wi=" 16 | 17 | WEBMASTER = 'felinx.lee@gmail.com' 18 | ADMIN_EMAILS = ['felinx.lee@gmail.com', 'peterbe@gmail.com'] 19 | 20 | DEFAULT_DATABASE_NAME = 'tornadogists' 21 | 22 | OAUTH_SETTINGS = { 23 | 'client_id': '505fee7706f024a2bcc1', 24 | 'client_secret': open(os.path.join(_ROOT, 'github_client_secret')).read().strip(), 25 | 'base_url': 'https://github.com/login/oauth/', 26 | 'redirect_url': 'http://tornadogists.org/auth/github/' # can this be localhost:8000/...? 27 | } 28 | 29 | try: 30 | from local_settings import * 31 | except ImportError: 32 | pass 33 | -------------------------------------------------------------------------------- /static/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: white; 3 | color: black; 4 | font-family: Georgia, serif; 5 | font-size: 11pt; 6 | margin: 10px; 7 | margin-top: 15px; 8 | margin-bottom: 15px; 9 | } 10 | 11 | h1, 12 | h2, 13 | h3, 14 | h4 { 15 | font-family: Calibri, sans-serif; 16 | margin: 0; 17 | } 18 | 19 | img { 20 | border: 0; 21 | } 22 | 23 | pre, 24 | code { 25 | color: #060; 26 | } 27 | 28 | a, 29 | a code { 30 | color: #216093; 31 | } 32 | 33 | table { 34 | border-collapse: collapse; 35 | border: 0; 36 | } 37 | 38 | td { 39 | border: 0; 40 | padding: 0; 41 | } 42 | 43 | #body { 44 | margin: auto; 45 | max-width: 850px; 46 | } 47 | 48 | #header { 49 | margin-bottom: 15px; 50 | margin-right: 30px; 51 | } 52 | 53 | #content, 54 | #footer { 55 | margin-left: 31px; 56 | margin-right: 31px; 57 | } 58 | 59 | #content p, 60 | #content li, 61 | #footer { 62 | line-height: 16pt; 63 | } 64 | 65 | #content pre { 66 | line-height: 14pt; 67 | margin: 17pt; 68 | padding-left: 1em; 69 | border-left: 1px solid #ccc; 70 | } 71 | 72 | #footer { 73 | margin-top: 2.5em; 74 | border-top: 1px solid #ccc; 75 | text-align:center; 76 | padding-top:0.5em; 77 | font-size:14px; 78 | } 79 | 80 | #header .logo { 81 | line-height: 0; 82 | padding-bottom: 5px; 83 | padding-right: 15px; 84 | } 85 | 86 | #header .logo img { 87 | width: 286px; 88 | height: 72px; 89 | } 90 | 91 | #header .title { 92 | vertical-align: bottom; 93 | } 94 | 95 | #header .title h1 { 96 | font-size: 35px; 97 | font-weight: normal; 98 | } 99 | 100 | #header .title h1, 101 | #header .title h1 a { 102 | color: #666; 103 | } 104 | 105 | #content h1, 106 | #content h2, 107 | #content h3 { 108 | color: #4d8cbf; 109 | margin-bottom: 2pt; 110 | margin-top: 17pt; 111 | } 112 | 113 | #content h2 { 114 | font-size: 19pt; 115 | } 116 | 117 | #content h3 { 118 | font-size: 15pt; 119 | } 120 | 121 | #content p { 122 | margin: 0; 123 | margin-bottom: 1em; 124 | } 125 | 126 | .hidden { 127 | display:none; 128 | } -------------------------------------------------------------------------------- /static/css/ext/fancybox/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/blank.gif -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_close.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_loading.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_nav_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_nav_left.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_nav_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_nav_right.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_e.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_n.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_ne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_ne.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_nw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_nw.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_s.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_se.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_sw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_sw.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_shadow_w.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_title_left.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_title_main.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_over.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_title_over.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancy_title_right.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancybox-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancybox-x.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancybox-y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancybox-y.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancybox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/fancybox/fancybox.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/jquery.fancybox-1.3.4.css: -------------------------------------------------------------------------------- 1 | /* 2 | * FancyBox - jQuery Plugin 3 | * Simple and fancy lightbox alternative 4 | * 5 | * Examples and documentation at: http://fancybox.net 6 | * 7 | * Copyright (c) 2008 - 2010 Janis Skarnelis 8 | * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. 9 | * 10 | * Version: 1.3.4 (11/11/2010) 11 | * Requires: jQuery v1.3+ 12 | * 13 | * Dual licensed under the MIT and GPL licenses: 14 | * http://www.opensource.org/licenses/mit-license.php 15 | * http://www.gnu.org/licenses/gpl.html 16 | */ 17 | 18 | #fancybox-loading { 19 | position: fixed; 20 | top: 50%; 21 | left: 50%; 22 | width: 40px; 23 | height: 40px; 24 | margin-top: -20px; 25 | margin-left: -20px; 26 | cursor: pointer; 27 | overflow: hidden; 28 | z-index: 1104; 29 | display: none; 30 | } 31 | 32 | #fancybox-loading div { 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | width: 40px; 37 | height: 480px; 38 | background-image: url('fancybox.png'); 39 | } 40 | 41 | #fancybox-overlay { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | z-index: 1100; 47 | display: none; 48 | } 49 | 50 | #fancybox-tmp { 51 | padding: 0; 52 | margin: 0; 53 | border: 0; 54 | overflow: auto; 55 | display: none; 56 | } 57 | 58 | #fancybox-wrap { 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | padding: 20px; 63 | z-index: 1101; 64 | outline: none; 65 | display: none; 66 | } 67 | 68 | #fancybox-outer { 69 | position: relative; 70 | width: 100%; 71 | height: 100%; 72 | background: #fff; 73 | } 74 | 75 | #fancybox-content { 76 | width: 0; 77 | height: 0; 78 | padding: 0; 79 | outline: none; 80 | position: relative; 81 | overflow: hidden; 82 | z-index: 1102; 83 | border: 0px solid #fff; 84 | } 85 | 86 | #fancybox-hide-sel-frame { 87 | position: absolute; 88 | top: 0; 89 | left: 0; 90 | width: 100%; 91 | height: 100%; 92 | background: transparent; 93 | z-index: 1101; 94 | } 95 | 96 | #fancybox-close { 97 | position: absolute; 98 | top: -15px; 99 | right: -15px; 100 | width: 30px; 101 | height: 30px; 102 | background: transparent url('fancybox.png') -40px 0px; 103 | cursor: pointer; 104 | z-index: 1103; 105 | display: none; 106 | } 107 | 108 | #fancybox-error { 109 | color: #444; 110 | font: normal 12px/20px Arial; 111 | padding: 14px; 112 | margin: 0; 113 | } 114 | 115 | #fancybox-img { 116 | width: 100%; 117 | height: 100%; 118 | padding: 0; 119 | margin: 0; 120 | border: none; 121 | outline: none; 122 | line-height: 0; 123 | vertical-align: top; 124 | } 125 | 126 | #fancybox-frame { 127 | width: 100%; 128 | height: 100%; 129 | border: none; 130 | display: block; 131 | } 132 | 133 | #fancybox-left, #fancybox-right { 134 | position: absolute; 135 | bottom: 0px; 136 | height: 100%; 137 | width: 35%; 138 | cursor: pointer; 139 | outline: none; 140 | background: transparent url('blank.gif'); 141 | z-index: 1102; 142 | display: none; 143 | } 144 | 145 | #fancybox-left { 146 | left: 0px; 147 | } 148 | 149 | #fancybox-right { 150 | right: 0px; 151 | } 152 | 153 | #fancybox-left-ico, #fancybox-right-ico { 154 | position: absolute; 155 | top: 50%; 156 | left: -9999px; 157 | width: 30px; 158 | height: 30px; 159 | margin-top: -15px; 160 | cursor: pointer; 161 | z-index: 1102; 162 | display: block; 163 | } 164 | 165 | #fancybox-left-ico { 166 | background-image: url('fancybox.png'); 167 | background-position: -40px -30px; 168 | } 169 | 170 | #fancybox-right-ico { 171 | background-image: url('fancybox.png'); 172 | background-position: -40px -60px; 173 | } 174 | 175 | #fancybox-left:hover, #fancybox-right:hover { 176 | visibility: visible; /* IE6 */ 177 | } 178 | 179 | #fancybox-left:hover span { 180 | left: 20px; 181 | } 182 | 183 | #fancybox-right:hover span { 184 | left: auto; 185 | right: 20px; 186 | } 187 | 188 | .fancybox-bg { 189 | position: absolute; 190 | padding: 0; 191 | margin: 0; 192 | border: 0; 193 | width: 20px; 194 | height: 20px; 195 | z-index: 1001; 196 | } 197 | 198 | #fancybox-bg-n { 199 | top: -20px; 200 | left: 0; 201 | width: 100%; 202 | background-image: url('fancybox-x.png'); 203 | } 204 | 205 | #fancybox-bg-ne { 206 | top: -20px; 207 | right: -20px; 208 | background-image: url('fancybox.png'); 209 | background-position: -40px -162px; 210 | } 211 | 212 | #fancybox-bg-e { 213 | top: 0; 214 | right: -20px; 215 | height: 100%; 216 | background-image: url('fancybox-y.png'); 217 | background-position: -20px 0px; 218 | } 219 | 220 | #fancybox-bg-se { 221 | bottom: -20px; 222 | right: -20px; 223 | background-image: url('fancybox.png'); 224 | background-position: -40px -182px; 225 | } 226 | 227 | #fancybox-bg-s { 228 | bottom: -20px; 229 | left: 0; 230 | width: 100%; 231 | background-image: url('fancybox-x.png'); 232 | background-position: 0px -20px; 233 | } 234 | 235 | #fancybox-bg-sw { 236 | bottom: -20px; 237 | left: -20px; 238 | background-image: url('fancybox.png'); 239 | background-position: -40px -142px; 240 | } 241 | 242 | #fancybox-bg-w { 243 | top: 0; 244 | left: -20px; 245 | height: 100%; 246 | background-image: url('fancybox-y.png'); 247 | } 248 | 249 | #fancybox-bg-nw { 250 | top: -20px; 251 | left: -20px; 252 | background-image: url('fancybox.png'); 253 | background-position: -40px -122px; 254 | } 255 | 256 | #fancybox-title { 257 | font-family: Helvetica; 258 | font-size: 12px; 259 | z-index: 1102; 260 | } 261 | 262 | .fancybox-title-inside { 263 | padding-bottom: 10px; 264 | text-align: center; 265 | color: #333; 266 | background: #fff; 267 | position: relative; 268 | } 269 | 270 | .fancybox-title-outside { 271 | padding-top: 10px; 272 | color: #fff; 273 | } 274 | 275 | .fancybox-title-over { 276 | position: absolute; 277 | bottom: 0; 278 | left: 0; 279 | color: #FFF; 280 | text-align: left; 281 | } 282 | 283 | #fancybox-title-over { 284 | padding: 10px; 285 | background-image: url('fancy_title_over.png'); 286 | display: block; 287 | } 288 | 289 | .fancybox-title-float { 290 | position: absolute; 291 | left: 0; 292 | bottom: -20px; 293 | height: 32px; 294 | } 295 | 296 | #fancybox-title-float-wrap { 297 | border: none; 298 | border-collapse: collapse; 299 | width: auto; 300 | } 301 | 302 | #fancybox-title-float-wrap td { 303 | border: none; 304 | white-space: nowrap; 305 | } 306 | 307 | #fancybox-title-float-left { 308 | padding: 0 0 0 15px; 309 | background: url('fancybox.png') -40px -90px no-repeat; 310 | } 311 | 312 | #fancybox-title-float-main { 313 | color: #FFF; 314 | line-height: 29px; 315 | font-weight: bold; 316 | padding: 0 0 3px 0; 317 | background: url('fancybox-x.png') 0px -40px; 318 | } 319 | 320 | #fancybox-title-float-right { 321 | padding: 0 0 0 15px; 322 | background: url('fancybox.png') -55px -90px no-repeat; 323 | } 324 | 325 | /* IE6 */ 326 | 327 | .fancybox-ie6 #fancybox-close { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_close.png', sizingMethod='scale'); } 328 | 329 | .fancybox-ie6 #fancybox-left-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_left.png', sizingMethod='scale'); } 330 | .fancybox-ie6 #fancybox-right-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_right.png', sizingMethod='scale'); } 331 | 332 | .fancybox-ie6 #fancybox-title-over { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_over.png', sizingMethod='scale'); zoom: 1; } 333 | .fancybox-ie6 #fancybox-title-float-left { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_left.png', sizingMethod='scale'); } 334 | .fancybox-ie6 #fancybox-title-float-main { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_main.png', sizingMethod='scale'); } 335 | .fancybox-ie6 #fancybox-title-float-right { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_right.png', sizingMethod='scale'); } 336 | 337 | .fancybox-ie6 #fancybox-bg-w, .fancybox-ie6 #fancybox-bg-e, .fancybox-ie6 #fancybox-left, .fancybox-ie6 #fancybox-right, #fancybox-hide-sel-frame { 338 | height: expression(this.parentNode.clientHeight + "px"); 339 | } 340 | 341 | #fancybox-loading.fancybox-ie6 { 342 | position: absolute; margin-top: 0; 343 | top: expression( (-20 + (document.documentElement.clientHeight ? document.documentElement.clientHeight/2 : document.body.clientHeight/2 ) + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop )) + 'px'); 344 | } 345 | 346 | #fancybox-loading.fancybox-ie6 div { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_loading.png', sizingMethod='scale'); } 347 | 348 | /* IE6, IE7, IE8 */ 349 | 350 | .fancybox-ie .fancybox-bg { background: transparent !important; } 351 | 352 | .fancybox-ie #fancybox-bg-n { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_n.png', sizingMethod='scale'); } 353 | .fancybox-ie #fancybox-bg-ne { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_ne.png', sizingMethod='scale'); } 354 | .fancybox-ie #fancybox-bg-e { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_e.png', sizingMethod='scale'); } 355 | .fancybox-ie #fancybox-bg-se { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_se.png', sizingMethod='scale'); } 356 | .fancybox-ie #fancybox-bg-s { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_s.png', sizingMethod='scale'); } 357 | .fancybox-ie #fancybox-bg-sw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_sw.png', sizingMethod='scale'); } 358 | .fancybox-ie #fancybox-bg-w { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_w.png', sizingMethod='scale'); } 359 | .fancybox-ie #fancybox-bg-nw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_nw.png', sizingMethod='scale'); } -------------------------------------------------------------------------------- /static/css/ext/indicator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/css/ext/indicator.gif -------------------------------------------------------------------------------- /static/css/ext/jquery.autocomplete.css: -------------------------------------------------------------------------------- 1 | .ac_results { 2 | padding: 0px; 3 | border: 1px solid black; 4 | background-color: white; 5 | overflow: hidden; 6 | z-index: 99999; 7 | } 8 | 9 | .ac_results ul { 10 | width: 100%; 11 | list-style-position: outside; 12 | list-style: none; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | .ac_results li { 18 | margin: 0px; 19 | padding: 2px 5px; 20 | cursor: default; 21 | display: block; 22 | /* 23 | if width will be 100% horizontal scrollbar will apear 24 | when scroll mode will be used 25 | */ 26 | /*width: 100%;*/ 27 | font: menu; 28 | font-size: 12px; 29 | /* 30 | it is very important, if line-height not setted or setted 31 | in relative units scroll will be broken in firefox 32 | */ 33 | line-height: 16px; 34 | overflow: hidden; 35 | } 36 | 37 | .ac_loading { 38 | background: white url('indicator.gif') right center no-repeat; 39 | } 40 | 41 | .ac_odd { 42 | background-color: #eee; 43 | } 44 | 45 | .ac_over { 46 | background-color: #0A246A; 47 | color: white; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /static/css/ext/jquery.qtip.css: -------------------------------------------------------------------------------- 1 | /* 2 | * qTip2 - Pretty powerful tooltips 3 | * http://craigsworks.com/projects/qtip2/ 4 | * 5 | * Version: nightly 6 | * Copyright 2009-2010 Craig Michael Thompson - http://craigsworks.com 7 | * 8 | * Dual licensed under MIT or GPLv2 licenses 9 | * http://en.wikipedia.org/wiki/MIT_License 10 | * http://en.wikipedia.org/wiki/GNU_General_Public_License 11 | * 12 | * Date: Thu Mar 31 14:48:34 PDT 2011 13 | */ 14 | 15 | /* Fluid class for determining actual width in IE */ 16 | .ui-tooltip-fluid{ 17 | display: block; 18 | visibility: hidden; 19 | position: static !important; 20 | float: left !important; 21 | } 22 | 23 | .ui-tooltip, .qtip{ 24 | position: absolute; 25 | left: -31000px; 26 | top: -31000px; 27 | display: none; 28 | 29 | max-width: 280px; 30 | min-width: 50px; 31 | 32 | font-size: 10.5px; 33 | line-height: 12px; 34 | } 35 | 36 | .ui-tooltip-content{ 37 | position: relative; 38 | padding: 5px 9px; 39 | overflow: hidden; 40 | 41 | border-width: 1px; 42 | border-style: solid; 43 | 44 | text-align: left; 45 | word-wrap: break-word; 46 | overflow: hidden; 47 | } 48 | 49 | .ui-tooltip-titlebar{ 50 | position: relative; 51 | min-height: 14px; 52 | padding: 5px 35px 5px 10px; 53 | overflow: hidden; 54 | 55 | border-width: 1px 1px 0; 56 | border-style: solid; 57 | 58 | font-weight: bold; 59 | } 60 | 61 | .ui-tooltip-titlebar + .ui-tooltip-content{ border-top-width: 0px !important; } 62 | 63 | /*! Default close button class */ 64 | .ui-tooltip-titlebar .ui-state-default{ 65 | position: absolute; 66 | right: 4px; 67 | top: 50%; 68 | margin-top: -9px; 69 | 70 | cursor: pointer; 71 | outline: medium none; 72 | 73 | border-width: 1px; 74 | border-style: solid; 75 | } 76 | 77 | * html .ui-tooltip-titlebar .ui-state-default{ 78 | top: 16px; 79 | } 80 | 81 | .ui-tooltip-titlebar .ui-icon, 82 | .ui-tooltip-icon .ui-icon{ 83 | display: block; 84 | text-indent: -1000em; 85 | } 86 | 87 | .ui-tooltip-icon, .ui-tooltip-icon .ui-icon{ 88 | -moz-border-radius: 3px; 89 | -webkit-border-radius: 3px; 90 | border-radius: 3px; 91 | } 92 | 93 | .ui-tooltip-icon .ui-icon{ 94 | width: 18px; 95 | height: 14px; 96 | 97 | text-align: center; 98 | text-indent: 0; 99 | font: normal bold 10px/13px Tahoma,sans-serif; 100 | 101 | color: inherit; 102 | background: transparent none no-repeat -100em -100em; 103 | } 104 | 105 | 106 | /* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */ 107 | .ui-tooltip-focus{ 108 | 109 | } 110 | 111 | 112 | /*! Default tooltip style */ 113 | .ui-tooltip-titlebar, 114 | .ui-tooltip-content{ 115 | border-color: #F1D031; 116 | background-color: #FFFFA3; 117 | color: #555; 118 | } 119 | 120 | .ui-tooltip-titlebar{ 121 | background-color: #FFEF93; 122 | } 123 | 124 | .ui-tooltip-titlebar .ui-tooltip-icon{ 125 | border-color: #CCC; 126 | background: #F1F1F1; 127 | color: #777; 128 | } 129 | 130 | .ui-tooltip-titlebar .ui-state-hover{ 131 | border-color: #AAA; 132 | color: #111; 133 | } 134 | 135 | 136 | /*! Light tooltip style */ 137 | .ui-tooltip-light .ui-tooltip-titlebar, 138 | .ui-tooltip-light .ui-tooltip-content{ 139 | border-color: #E2E2E2; 140 | color: #454545; 141 | } 142 | 143 | .ui-tooltip-light .ui-tooltip-content{ 144 | background-color: white; 145 | } 146 | 147 | .ui-tooltip-light .ui-tooltip-titlebar{ 148 | background-color: #f1f1f1; 149 | } 150 | 151 | 152 | /*! Dark tooltip style */ 153 | .ui-tooltip-dark .ui-tooltip-titlebar, 154 | .ui-tooltip-dark .ui-tooltip-content{ 155 | border-color: #303030; 156 | color: #f3f3f3; 157 | } 158 | 159 | .ui-tooltip-dark .ui-tooltip-content{ 160 | background-color: #505050; 161 | } 162 | 163 | .ui-tooltip-dark .ui-tooltip-titlebar{ 164 | background-color: #404040; 165 | } 166 | 167 | .ui-tooltip-dark .ui-tooltip-icon{ 168 | border-color: #444; 169 | } 170 | 171 | .ui-tooltip-dark .ui-tooltip-titlebar .ui-state-hover{ 172 | border-color: #303030; 173 | } 174 | 175 | 176 | /*! Cream tooltip style */ 177 | .ui-tooltip-cream .ui-tooltip-titlebar, 178 | .ui-tooltip-cream .ui-tooltip-content{ 179 | border-color: #F9E98E; 180 | color: #A27D35; 181 | } 182 | 183 | .ui-tooltip-cream .ui-tooltip-content{ 184 | background-color: #FBF7AA; 185 | } 186 | 187 | .ui-tooltip-cream .ui-tooltip-titlebar{ 188 | background-color: #F0DE7D; 189 | } 190 | 191 | .ui-tooltip-cream .ui-state-default .ui-tooltip-icon{ 192 | background-position: -82px 0; 193 | } 194 | 195 | 196 | /*! Red tooltip style */ 197 | .ui-tooltip-red .ui-tooltip-titlebar, 198 | .ui-tooltip-red .ui-tooltip-content{ 199 | border-color: #D95252; 200 | color: #912323; 201 | } 202 | 203 | .ui-tooltip-red .ui-tooltip-content{ 204 | background-color: #F78B83; 205 | } 206 | 207 | .ui-tooltip-red .ui-tooltip-titlebar{ 208 | background-color: #F06D65; 209 | } 210 | 211 | .ui-tooltip-red .ui-state-default .ui-tooltip-icon{ 212 | background-position: -102px 0; 213 | } 214 | 215 | .ui-tooltip-red .ui-tooltip-icon{ 216 | border-color: #D95252; 217 | } 218 | 219 | .ui-tooltip-red .ui-tooltip-titlebar .ui-state-hover{ 220 | border-color: #D95252; 221 | } 222 | 223 | 224 | /*! Green tooltip style */ 225 | .ui-tooltip-green .ui-tooltip-titlebar, 226 | .ui-tooltip-green .ui-tooltip-content{ 227 | border-color: #90D93F; 228 | color: #3F6219; 229 | } 230 | 231 | .ui-tooltip-green .ui-tooltip-content{ 232 | background-color: #CAED9E; 233 | } 234 | 235 | .ui-tooltip-green .ui-tooltip-titlebar{ 236 | background-color: #B0DE78; 237 | } 238 | 239 | .ui-tooltip-green .ui-state-default .ui-tooltip-icon{ 240 | background-position: -42px 0; 241 | } 242 | 243 | 244 | /*! Blue tooltip style */ 245 | .ui-tooltip-blue .ui-tooltip-titlebar, 246 | .ui-tooltip-blue .ui-tooltip-content{ 247 | border-color: #ADD9ED; 248 | color: #5E99BD; 249 | } 250 | 251 | .ui-tooltip-blue .ui-tooltip-content{ 252 | background-color: #E5F6FE; 253 | } 254 | 255 | .ui-tooltip-blue .ui-tooltip-titlebar{ 256 | background-color: #D0E9F5; 257 | } 258 | 259 | .ui-tooltip-blue .ui-state-default .ui-tooltip-icon{ 260 | background-position: -2px 0; 261 | }.ui-tooltip .ui-tooltip-tip{ 262 | margin: 0 auto; 263 | overflow: hidden; 264 | 265 | background: transparent !important; 266 | border: 0px dashed transparent !important; 267 | z-index: 10; 268 | } 269 | 270 | .ui-tooltip .ui-tooltip-tip, 271 | .ui-tooltip .ui-tooltip-tip *{ 272 | position: absolute; 273 | 274 | line-height: 0.1px !important; 275 | font-size: 0.1px !important; 276 | color: #123456; 277 | 278 | background: transparent; 279 | border: 0px dashed transparent; 280 | } 281 | 282 | #qtip-overlay{ 283 | position: absolute; 284 | left: -10000em; 285 | top: -10000em; 286 | 287 | background-color: black; 288 | 289 | opacity: 0.7; 290 | filter:alpha(opacity=70); 291 | -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; 292 | } 293 | 294 | /*! Add shadows to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE6+, Safari 2+ */ 295 | .ui-tooltip-shadow{ 296 | -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 297 | -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 298 | box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 299 | } 300 | 301 | .ui-tooltip-shadow .ui-tooltip-titlebar, 302 | .ui-tooltip-shadow .ui-tooltip-content{ 303 | filter: progid:DXImageTransform.Microsoft.Shadow(Color='gray', Direction=135, Strength=3); 304 | -ms-filter:"progid:DXImageTransform.Microsoft.Shadow(Color='gray', Direction=135, Strength=3)"; 305 | } 306 | 307 | 308 | /*! Add rounded corners to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */ 309 | .ui-tooltip-rounded, 310 | .ui-tooltip-rounded .ui-tooltip-content, 311 | .ui-tooltip-tipsy, 312 | .ui-tooltip-tipsy .ui-tooltip-content, 313 | .ui-tooltip-youtube, 314 | .ui-tooltip-youtube .ui-tooltip-content{ 315 | -moz-border-radius: 4px; 316 | -webkit-border-radius: 4px; 317 | border-radius: 4px; 318 | } 319 | 320 | .ui-tooltip-rounded .ui-tooltip-titlebar, 321 | .ui-tooltip-tipsy .ui-tooltip-titlebar, 322 | .ui-tooltip-youtube .ui-tooltip-titlebar{ 323 | -moz-border-radius: 5px 5px 0 0; 324 | -webkit-border-radius: 5px 5px 0 0; 325 | border-radius: 5px 5px 0 0; 326 | } 327 | 328 | .ui-tooltip-rounded .ui-tooltip-titlebar + .ui-tooltip-content, 329 | .ui-tooltip-tipsy .ui-tooltip-titlebar + .ui-tooltip-content, 330 | .ui-tooltip-youtube .ui-tooltip-titlebar + .ui-tooltip-content{ 331 | -moz-border-radius: 0 0 5px 5px; 332 | -webkit-border-radius: 0 0 5px 5px; 333 | border-radius: 0 0 5px 5px; 334 | } 335 | 336 | 337 | /*! Youtube tooltip style */ 338 | .ui-tooltip-youtube{ 339 | -webkit-box-shadow: 0 0 3px #333; 340 | -moz-box-shadow: 0 0 3px #333; 341 | box-shadow: 0 0 3px #333; 342 | } 343 | 344 | .ui-tooltip-youtube .ui-tooltip-titlebar, 345 | .ui-tooltip-youtube .ui-tooltip-content{ 346 | background: transparent; 347 | background: rgba(0, 0, 0, 0.85); 348 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000); 349 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000)"; 350 | 351 | color: white; 352 | border-color: #CCCCCC; 353 | } 354 | 355 | .ui-tooltip-youtube .ui-tooltip-icon{ 356 | border-color: #222; 357 | } 358 | 359 | .ui-tooltip-youtube .ui-tooltip-titlebar .ui-state-hover{ 360 | border-color: #303030; 361 | } 362 | 363 | 364 | /* jQuery TOOLS Tooltip style */ 365 | .ui-tooltip-jtools{ 366 | background: #232323; 367 | background: rgba(0, 0, 0, 0.7); 368 | background-image: -moz-linear-gradient(top, #717171, #232323); 369 | background-image: -webkit-gradient(linear, left top, left bottom, from(#717171), to(#232323)); 370 | 371 | border: 2px solid #ddd; 372 | border: 2px solid rgba(241,241,241,1); 373 | 374 | -moz-border-radius: 2px; 375 | -webkit-border-radius: 2px; 376 | border-radius: 2px; 377 | 378 | -webkit-box-shadow: 0 0 12px #333; 379 | -moz-box-shadow: 0 0 12px #333; 380 | box-shadow: 0 0 12px #333; 381 | } 382 | /* IE Specific */ 383 | .ui-tooltip-jtools .ui-tooltip-titlebar{ 384 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A); 385 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"; 386 | } 387 | 388 | .ui-tooltip-jtools .ui-tooltip-content{ 389 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323); 390 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"; 391 | } 392 | 393 | .ui-tooltip-jtools .ui-tooltip-titlebar, 394 | .ui-tooltip-jtools .ui-tooltip-content{ 395 | background: transparent; 396 | color: white; 397 | border: 0 dashed transparent; 398 | } 399 | 400 | .ui-tooltip-jtools .ui-tooltip-icon{ 401 | border-color: #555; 402 | } 403 | 404 | .ui-tooltip-jtools .ui-tooltip-titlebar .ui-state-hover{ 405 | border-color: #333; 406 | } 407 | 408 | 409 | /* Cluetip style */ 410 | .ui-tooltip-cluetip{ 411 | -webkit-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 412 | -moz-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 413 | box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 414 | } 415 | 416 | .ui-tooltip-cluetip .ui-tooltip-titlebar{ 417 | background-color: #87876A; 418 | color: white; 419 | border: 0 dashed transparent; 420 | } 421 | 422 | .ui-tooltip-cluetip .ui-tooltip-content{ 423 | background-color: #D9D9C2; 424 | color: #111; 425 | border: 0 dashed transparent; 426 | } 427 | 428 | .ui-tooltip-cluetip .ui-tooltip-icon{ 429 | border-color: #808064; 430 | } 431 | 432 | .ui-tooltip-cluetip .ui-tooltip-titlebar .ui-state-hover{ 433 | border-color: #696952; 434 | color: #696952; 435 | } 436 | 437 | 438 | /* Tipsy style */ 439 | .ui-tooltip-tipsy{ 440 | border: 0 solid #000; 441 | border: 0 solid rgba(0,0,0,.87); 442 | } 443 | 444 | .ui-tooltip-tipsy .ui-tooltip-titlebar, 445 | .ui-tooltip-tipsy .ui-tooltip-content{ 446 | background: transparent; 447 | background: rgba(0, 0, 0, .87); 448 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000); 449 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000)"; 450 | 451 | color: white; 452 | border: 0 dashed transparent; 453 | 454 | font-size: 11px; 455 | font-family: 'Lucida Grande', sans-serif; 456 | font-weight: bold; 457 | line-height: 16px; 458 | text-shadow: 0 1px black; 459 | } 460 | 461 | .ui-tooltip-tipsy .ui-tooltip-titlebar{ 462 | padding: 6px 35px 0 10; 463 | } 464 | 465 | .ui-tooltip-tipsy .ui-tooltip-content{ 466 | padding: 6px 10; 467 | } 468 | 469 | .ui-tooltip-tipsy .ui-tooltip-icon{ 470 | border-color: #222; 471 | text-shadow: none; 472 | } 473 | 474 | .ui-tooltip-tipsy .ui-tooltip-titlebar .ui-state-hover{ 475 | border-color: #303030; 476 | } 477 | 478 | 479 | /* Tipped style */ 480 | .ui-tooltip-tipped{ 481 | 482 | } 483 | 484 | .ui-tooltip-tipped .ui-tooltip-titlebar, 485 | .ui-tooltip-tipped .ui-tooltip-content{ 486 | border: 3px solid #959FA9; 487 | } 488 | 489 | .ui-tooltip-tipped .ui-tooltip-titlebar{ 490 | background: #3A79B8; 491 | background-image: -moz-linear-gradient(top, #3A79B8, #2E629D); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3A79B8), to(#2E629D)); 493 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D); 494 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"; 495 | 496 | color: white; 497 | font-weight: normal; 498 | font-family: serif; 499 | 500 | border-bottom-width: 0; 501 | -moz-border-radius: 3px 3px 0 0; 502 | -webkit-border-radius: 3px 3px 0 0; 503 | border-radius: 3px 3px 0 0; 504 | } 505 | 506 | .ui-tooltip-tipped .ui-tooltip-content{ 507 | background-color: #F9F9F9; 508 | color: #454545; 509 | 510 | -moz-border-radius: 0 0 3px 3px; 511 | -webkit-border-radius: 0 0 3px 3px; 512 | border-radius: 0 0 3px 3px; 513 | } 514 | 515 | .ui-tooltip-tipped .ui-tooltip-icon{ 516 | border: 2px solid #285589; 517 | background: #285589; 518 | } 519 | 520 | .ui-tooltip-tipped .ui-tooltip-icon .ui-icon{ 521 | background-color: #FBFBFB; 522 | color: #555; 523 | } 524 | -------------------------------------------------------------------------------- /static/css/ext/jquery.qtip.min.css: -------------------------------------------------------------------------------- 1 | .ui-tooltip-fluid{display:block;visibility:hidden;position:static!important;float:left!important;}.ui-tooltip,.qtip{position:absolute;left:-31000px;top:-31000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;}.ui-tooltip-content{position:relative;padding:5px 9px;overflow:hidden;border-width:1px;border-style:solid;text-align:left;word-wrap:break-word;overflow:hidden;}.ui-tooltip-titlebar{position:relative;min-height:14px;padding:5px 35px 5px 10px;overflow:hidden;border-width:1px 1px 0;border-style:solid;font-weight:bold;}.ui-tooltip-titlebar+.ui-tooltip-content{border-top-width:0!important;}/*!Default close button class */ .ui-tooltip-titlebar .ui-state-default{position:absolute;right:4px;top:50%;margin-top:-9px;cursor:pointer;outline:medium none;border-width:1px;border-style:solid;}* html .ui-tooltip-titlebar .ui-state-default{top:16px;}.ui-tooltip-titlebar .ui-icon,.ui-tooltip-icon .ui-icon{display:block;text-indent:-1000em;}.ui-tooltip-icon,.ui-tooltip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;}.ui-tooltip-icon .ui-icon{width:18px;height:14px;text-align:center;text-indent:0;font:normal bold 10px/13px Tahoma,sans-serif;color:inherit;background:transparent none no-repeat -100em -100em;}/*!Default tooltip style */ .ui-tooltip-titlebar,.ui-tooltip-content{border-color:#F1D031;background-color:#FFFFA3;color:#555;}.ui-tooltip-titlebar{background-color:#FFEF93;}.ui-tooltip-titlebar .ui-tooltip-icon{border-color:#CCC;background:#F1F1F1;color:#777;}.ui-tooltip-titlebar .ui-state-hover{border-color:#AAA;color:#111;}/*!Light tooltip style */ .ui-tooltip-light .ui-tooltip-titlebar,.ui-tooltip-light .ui-tooltip-content{border-color:#E2E2E2;color:#454545;}.ui-tooltip-light .ui-tooltip-content{background-color:white;}.ui-tooltip-light .ui-tooltip-titlebar{background-color:#f1f1f1;}/*!Dark tooltip style */ .ui-tooltip-dark .ui-tooltip-titlebar,.ui-tooltip-dark .ui-tooltip-content{border-color:#303030;color:#f3f3f3;}.ui-tooltip-dark .ui-tooltip-content{background-color:#505050;}.ui-tooltip-dark .ui-tooltip-titlebar{background-color:#404040;}.ui-tooltip-dark .ui-tooltip-icon{border-color:#444;}.ui-tooltip-dark .ui-tooltip-titlebar .ui-state-hover{border-color:#303030;}/*!Cream tooltip style */ .ui-tooltip-cream .ui-tooltip-titlebar,.ui-tooltip-cream .ui-tooltip-content{border-color:#F9E98E;color:#A27D35;}.ui-tooltip-cream .ui-tooltip-content{background-color:#FBF7AA;}.ui-tooltip-cream .ui-tooltip-titlebar{background-color:#F0DE7D;}.ui-tooltip-cream .ui-state-default .ui-tooltip-icon{background-position:-82px 0;}/*!Red tooltip style */ .ui-tooltip-red .ui-tooltip-titlebar,.ui-tooltip-red .ui-tooltip-content{border-color:#D95252;color:#912323;}.ui-tooltip-red .ui-tooltip-content{background-color:#F78B83;}.ui-tooltip-red .ui-tooltip-titlebar{background-color:#F06D65;}.ui-tooltip-red .ui-state-default .ui-tooltip-icon{background-position:-102px 0;}.ui-tooltip-red .ui-tooltip-icon{border-color:#D95252;}.ui-tooltip-red .ui-tooltip-titlebar .ui-state-hover{border-color:#D95252;}/*!Green tooltip style */ .ui-tooltip-green .ui-tooltip-titlebar,.ui-tooltip-green .ui-tooltip-content{border-color:#90D93F;color:#3F6219;}.ui-tooltip-green .ui-tooltip-content{background-color:#CAED9E;}.ui-tooltip-green .ui-tooltip-titlebar{background-color:#B0DE78;}.ui-tooltip-green .ui-state-default .ui-tooltip-icon{background-position:-42px 0;}/*!Blue tooltip style */ .ui-tooltip-blue .ui-tooltip-titlebar,.ui-tooltip-blue .ui-tooltip-content{border-color:#ADD9ED;color:#5E99BD;}.ui-tooltip-blue .ui-tooltip-content{background-color:#E5F6FE;}.ui-tooltip-blue .ui-tooltip-titlebar{background-color:#D0E9F5;}.ui-tooltip-blue .ui-state-default .ui-tooltip-icon{background-position:-2px 0;}.ui-tooltip .ui-tooltip-tip{margin:0 auto;overflow:hidden;background:transparent!important;border:0 dashed transparent!important;z-index:10;}.ui-tooltip .ui-tooltip-tip,.ui-tooltip .ui-tooltip-tip *{position:absolute;line-height:.1px!important;font-size:.1px!important;color:#123456;background:transparent;border:0 dashed transparent;}#qtip-overlay{position:absolute;left:-10000em;top:-10000em;background-color:black;opacity:.7;filter:alpha(opacity=70);-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";}/*!Add shadows to your tooltips in:FF3+,Chrome 2+,Opera 10.6+,IE6+,Safari 2+*/ .ui-tooltip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,0.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,0.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,0.15);}.ui-tooltip-shadow .ui-tooltip-titlebar,.ui-tooltip-shadow .ui-tooltip-content{filter:progid:DXImageTransform.Microsoft.Shadow(Color='gray',Direction=135,Strength=3);-ms-filter:"progid:DXImageTransform.Microsoft.Shadow(Color='gray',Direction=135,Strength=3)";}/*!Add rounded corners to your tooltips in:FF3+,Chrome 2+,Opera 10.6+,IE9+,Safari 2+*/ .ui-tooltip-rounded,.ui-tooltip-rounded .ui-tooltip-content,.ui-tooltip-tipsy,.ui-tooltip-tipsy .ui-tooltip-content,.ui-tooltip-youtube,.ui-tooltip-youtube .ui-tooltip-content{-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px;}.ui-tooltip-rounded .ui-tooltip-titlebar,.ui-tooltip-tipsy .ui-tooltip-titlebar,.ui-tooltip-youtube .ui-tooltip-titlebar{-moz-border-radius:5px 5px 0 0;-webkit-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;}.ui-tooltip-rounded .ui-tooltip-titlebar+.ui-tooltip-content,.ui-tooltip-tipsy .ui-tooltip-titlebar+.ui-tooltip-content,.ui-tooltip-youtube .ui-tooltip-titlebar+.ui-tooltip-content{-moz-border-radius:0 0 5px 5px;-webkit-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;}/*!Youtube tooltip style */ .ui-tooltip-youtube{-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;}.ui-tooltip-youtube .ui-tooltip-titlebar,.ui-tooltip-youtube .ui-tooltip-content{background:transparent;background:rgba(0,0,0,0.85);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000)";color:white;border-color:#CCC;}.ui-tooltip-youtube .ui-tooltip-icon{border-color:#222;}.ui-tooltip-youtube .ui-tooltip-titlebar .ui-state-hover{border-color:#303030;}.ui-tooltip-jtools{background:#232323;background:rgba(0,0,0,0.7);background-image:-moz-linear-gradient(top,#717171,#232323);background-image:-webkit-gradient(linear,left top,left bottom,from(#717171),to(#232323));border:2px solid #ddd;border:2px solid rgba(241,241,241,1);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333;}.ui-tooltip-jtools .ui-tooltip-titlebar{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)";}.ui-tooltip-jtools .ui-tooltip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)";}.ui-tooltip-jtools .ui-tooltip-titlebar,.ui-tooltip-jtools .ui-tooltip-content{background:transparent;color:white;border:0 dashed transparent;}.ui-tooltip-jtools .ui-tooltip-icon{border-color:#555;}.ui-tooltip-jtools .ui-tooltip-titlebar .ui-state-hover{border-color:#333;}.ui-tooltip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,0.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,0.4);box-shadow:4px 4px 5px rgba(0,0,0,0.4);}.ui-tooltip-cluetip .ui-tooltip-titlebar{background-color:#87876A;color:white;border:0 dashed transparent;}.ui-tooltip-cluetip .ui-tooltip-content{background-color:#D9D9C2;color:#111;border:0 dashed transparent;}.ui-tooltip-cluetip .ui-tooltip-icon{border-color:#808064;}.ui-tooltip-cluetip .ui-tooltip-titlebar .ui-state-hover{border-color:#696952;color:#696952;}.ui-tooltip-tipsy{border:0 solid #000;border:0 solid rgba(0,0,0,.87);}.ui-tooltip-tipsy .ui-tooltip-titlebar,.ui-tooltip-tipsy .ui-tooltip-content{background:transparent;background:rgba(0,0,0,.87);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#D9000000,endColorstr=#D9000000)";color:white;border:0 dashed transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:bold;line-height:16px;text-shadow:0 1px black;}.ui-tooltip-tipsy .ui-tooltip-titlebar{padding:6px 35px 0 10;}.ui-tooltip-tipsy .ui-tooltip-content{padding:6px 10;}.ui-tooltip-tipsy .ui-tooltip-icon{border-color:#222;text-shadow:none;}.ui-tooltip-tipsy .ui-tooltip-titlebar .ui-state-hover{border-color:#303030;}.ui-tooltip-tipped .ui-tooltip-titlebar,.ui-tooltip-tipped .ui-tooltip-content{border:3px solid #959FA9;}.ui-tooltip-tipped .ui-tooltip-titlebar{background:#3A79B8;background-image:-moz-linear-gradient(top,#3A79B8,#2E629D);background-image:-webkit-gradient(linear,left top,left bottom,from(#3A79B8),to(#2E629D));filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)";color:white;font-weight:normal;font-family:serif;border-bottom-width:0;-moz-border-radius:3px 3px 0 0;-webkit-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;}.ui-tooltip-tipped .ui-tooltip-content{background-color:#F9F9F9;color:#454545;-moz-border-radius:0 0 3px 3px;-webkit-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;}.ui-tooltip-tipped .ui-tooltip-icon{border:2px solid #285589;background:#285589;}.ui-tooltip-tipped .ui-tooltip-icon .ui-icon{background-color:#FBFBFB;color:#555;} -------------------------------------------------------------------------------- /static/css/extra.css: -------------------------------------------------------------------------------- 1 | img.gravatar { 2 | border:1px solid #d0d0d0; 3 | padding:2px; 4 | } 5 | 6 | a.comment { 7 | display:block; 8 | float:right; 9 | background-image: url(/static/images/comment.png); 10 | background-repeat: no-repeat; 11 | padding: 0 0 0 25px; 12 | } 13 | 14 | .comment-count { 15 | background: none repeat scroll 0 0 #FF5F57; 16 | border-radius: 3px 3px 3px 3px; 17 | color: #FFFFFF; 18 | font-size: 11px; 19 | font-weight: bold; 20 | line-height: 10px; 21 | padding: 1px 3px 2px; 22 | /*position: absolute;*/ 23 | /*right: 72px;*/ 24 | text-shadow: 0 -1px #888888; 25 | /*top: 1px;*/ 26 | z-index: 0; 27 | } 28 | 29 | div.clearer {clear: both; line-height: 0; height: 0;} 30 | 31 | span.smalllove { 32 | font-size:10px; 33 | padding-top:0px; 34 | line-height:12px; 35 | color:white; 36 | font-weight:bold; 37 | display:block; 38 | text-align:center; 39 | width:16px; 40 | height:16px; 41 | background-image:url('/static/images/heart16.png'); 42 | background-repeat:no-repeat; 43 | } 44 | 45 | span.love { 46 | font-size:11px; 47 | color:white; 48 | font-weight:bold; 49 | display:block; 50 | text-align:center; 51 | width:22px; 52 | height:22px; 53 | background-image:url('/static/images/heart22.png'); 54 | background-repeat:no-repeat; 55 | } 56 | 57 | span.biglove { 58 | font-size:20px; 59 | padding-top:8px; 60 | color:white; 61 | font-weight:bold; 62 | display:block; 63 | text-align:center; 64 | width:48px; 65 | height:48px; 66 | background-image:url('/static/images/heart48.png'); 67 | background-repeat:no-repeat; 68 | } 69 | 70 | #most_loved_gists li { 71 | list-style-type: none; 72 | margin-left:-15px; 73 | } 74 | -------------------------------------------------------------------------------- /static/images/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/images/comment.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/heart16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/images/heart16.png -------------------------------------------------------------------------------- /static/images/heart22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/images/heart22.png -------------------------------------------------------------------------------- /static/images/heart48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/images/heart48.png -------------------------------------------------------------------------------- /static/images/tornado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tornado_gists/109f7a24ca96b904b0861805e6cd0b180ee69101/static/images/tornado.png -------------------------------------------------------------------------------- /static/js/ext/jquery.autocomplete.pack.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Autocomplete plugin 1.1 3 | * 4 | * Copyright (c) 2009 Jörn Zaefferer 5 | * 6 | * Dual licensed under the MIT and GPL licenses: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * http://www.gnu.org/licenses/gpl.html 9 | * 10 | * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $ 11 | */ 12 | eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}(';(3($){$.2e.1u({19:3(b,d){5 c=W b=="1B";d=$.1u({},$.M.1T,{Y:c?b:P,y:c?P:b,1J:c?$.M.1T.1J:10,X:d&&!d.1D?10:48},d);d.1y=d.1y||3(a){6 a};d.1v=d.1v||d.1R;6 A.I(3(){1M $.M(A,d)})},L:3(a){6 A.11("L",a)},1k:3(a){6 A.14("1k",[a])},2b:3(){6 A.14("2b")},28:3(a){6 A.14("28",[a])},24:3(){6 A.14("24")}});$.M=3(o,r){5 t={2Y:38,2S:40,2N:46,2I:9,2E:13,2B:27,2x:3I,2v:33,2p:34,2n:8};5 u=$(o).3r("19","3o").Q(r.2Q);5 p;5 m="";5 n=$.M.3c(r);5 s=0;5 k;5 h={1F:C};5 l=$.M.32(r,o,1Z,h);5 j;$.1Y.2X&&$(o.2U).11("45.19",3(){4(j){j=C;6 C}});u.11(($.1Y.2X?"43":"42")+".19",3(a){s=1;k=a.2M;3V(a.2M){O t.2Y:a.1d();4(l.N()){l.30()}w{12(0,D)}R;O t.2S:a.1d();4(l.N()){l.2D()}w{12(0,D)}R;O t.2v:a.1d();4(l.N()){l.2C()}w{12(0,D)}R;O t.2p:a.1d();4(l.N()){l.2A()}w{12(0,D)}R;O r.17&&$.1c(r.S)==","&&t.2x:O t.2I:O t.2E:4(1Z()){a.1d();j=D;6 C}R;O t.2B:l.Z();R;3J:1P(p);p=1O(12,r.1J);R}}).2t(3(){s++}).3E(3(){s=0;4(!h.1F){2r()}}).2q(3(){4(s++>1&&!l.N()){12(0,D)}}).11("1k",3(){5 c=(1r.7>1)?1r[1]:P;3 1N(q,a){5 b;4(a&&a.7){16(5 i=0;i1){5 f=r.S.7;5 c=$(o).18().1I;5 d,1H=0;$.I(b,3(i,a){1H+=a.7;4(c<=1H){d=i;6 C}1H+=f});b[d]=v;v=b.3f(r.S)}v+=r.S}u.K(v);1l();u.14("L",[e.y,e.F]);6 D}3 12(b,c){4(k==t.2N){l.Z();6}5 a=u.K();4(!c&&a==m)6;m=a;a=1m(a);4(a.7>=r.29){u.Q(r.26);4(!r.1s)a=a.J();21(a,3a,1l)}w{1q();l.Z()}};3 15(b){4(!b)6[""];4(!r.17)6[$.1c(b)];6 $.4h(b.23(r.S),3(a){6 $.1c(b).7?$.1c(a):P})}3 1m(a){4(!r.17)6 a;5 c=15(a);4(c.7==1)6 c[0];5 b=$(o).18().1I;4(b==a.7){c=15(a)}w{c=15(a.22(a.37(b),""))}6 c[c.7-1]}3 1G(q,a){4(r.1G&&(1m(u.K()).J()==q.J())&&k!=t.2n){u.K(u.K()+a.37(1m(m).7));$(o).18(m.7,m.7+a.7)}};3 2r(){1P(p);p=1O(1l,4g)};3 1l(){5 c=l.N();l.Z();1P(p);1q();4(r.36){u.1k(3(a){4(!a){4(r.17){5 b=15(u.K()).1n(0,-1);u.K(b.3f(r.S)+(b.7?r.S:""))}w{u.K("");u.14("L",P)}}})}};3 3a(q,a){4(a&&a.7&&s){1q();l.35(a,q);1G(q,a[0].F);l.20()}w{1l()}};3 21(f,d,g){4(!r.1s)f=f.J();5 e=n.31(f);4(e&&e.7){d(f,e)}w 4((W r.Y=="1B")&&(r.Y.7>0)){5 c={4f:+1M 4e()};$.I(r.2Z,3(a,b){c[a]=W b=="3"?b():b});$.4d({4c:"4b",4a:"19"+o.49,2V:r.2V,Y:r.Y,y:$.1u({q:1m(f),47:r.X},c),44:3(a){5 b=r.1A&&r.1A(a)||1A(a);n.1i(f,b);d(f,b)}})}w{l.2T();g(f)}};3 1A(c){5 d=[];5 b=c.23("\\n");16(5 i=0;i]*)("+a.22(/([\\^\\$\\(\\)\\[\\]\\{\\}\\*\\.\\+\\?\\|\\\\])/2K,"\\\\$1")+")(?![^<>]*>)(?![^&;]+;)","2K"),"<2J>$1")},1D:D,1E:3S};$.M.3c=3(g){5 h={};5 j=0;3 1f(s,a){4(!g.1s)s=s.J();5 i=s.2H(a);4(g.1w=="3R"){i=s.J().1k("\\\\b"+a.J())}4(i==-1)6 C;6 i==0||g.1w};3 1i(q,a){4(j>g.1g){1o()}4(!h[q]){j++}h[q]=a}3 1e(){4(!g.y)6 C;5 f={},2G=0;4(!g.Y)g.1g=1;f[""]=[];16(5 i=0,2F=g.y.7;i<2F;i++){5 c=g.y[i];c=(W c=="1B")?[c]:c;5 d=g.1v(c,i+1,g.y.7);4(d===C)1V;5 e=d.3Q(0).J();4(!f[e])f[e]=[];5 b={F:d,y:c,L:g.1z&&g.1z(c)||d};f[e].1U(b);4(2G++0){5 c=h[k];$.I(c,3(i,x){4(1f(x.F,q)){a.1U(x)}})}}6 a}w 4(h[q]){6 h[q]}w 4(g.1f){16(5 i=q.7-1;i>=g.29;i--){5 c=h[q.3O(0,i)];4(c){5 a=[];$.I(c,3(i,x){4(1f(x.F,q)){a[a.7]=x}});6 a}}}6 P}}};$.M.32=3(e,g,f,k){5 h={H:"3N"};5 j,z=-1,y,1t="",1S=D,G,B;3 2y(){4(!1S)6;G=$("<3M/>").Z().Q(e.2P).T("3L","3K").1Q(1K.2w);B=$("<3H/>").1Q(G).3G(3(a){4(U(a).2u&&U(a).2u.3F()==\'2s\'){z=$("1L",B).1h(h.H).3D(U(a));$(U(a)).Q(h.H)}}).2q(3(a){$(U(a)).Q(h.H);f();g.2t();6 C}).3C(3(){k.1F=D}).3B(3(){k.1F=C});4(e.E>0)G.T("E",e.E);1S=C}3 U(a){5 b=a.U;3A(b&&b.3z!="2s")b=b.3y;4(!b)6[];6 b}3 V(b){j.1n(z,z+1).1h(h.H);2o(b);5 a=j.1n(z,z+1).Q(h.H);4(e.1D){5 c=0;j.1n(0,z).I(3(){c+=A.1a});4((c+a[0].1a-B.1b())>B[0].3x){B.1b(c+a[0].1a-B.3w())}w 4(c=j.1j()){z=0}}3 2m(a){6 e.X&&e.X").3v(e.1y(a,1t)).Q(i%2==0?"3u":"3P").1Q(B)[0];$.y(c,"2k",y[i])}j=B.3t("1L");4(e.1X){j.1n(0,1).Q(h.H);z=0}4($.2e.2W)B.2W()}6{35:3(d,q){2y();y=d;1t=q;2l()},2D:3(){V(1)},30:3(){V(-1)},2C:3(){4(z!=0&&z-8<0){V(-z)}w{V(-8)}},2A:3(){4(z!=j.1j()-1&&z+8>j.1j()){V(j.1j()-1-z)}w{V(8)}},Z:3(){G&&G.Z();j&&j.1h(h.H);z=-1},N:3(){6 G&&G.3s(":N")},3q:3(){6 A.N()&&(j.2j("."+h.H)[0]||e.1X&&j[0])},20:3(){5 a=$(g).3p();G.T({E:W e.E=="1B"||e.E>0?e.E:$(g).E(),2i:a.2i+g.1a,1W:a.1W}).20();4(e.1D){B.1b(0);B.T({2L:e.1E,3n:\'3X\'});4($.1Y.3m&&W 1K.2w.3l.2L==="1x"){5 c=0;j.I(3(){c+=A.1a});5 b=c>e.1E;B.T(\'3k\',b?e.1E:c);4(!b){j.E(B.E()-2R(j.T("2O-1W"))-2R(j.T("2O-3j")))}}}},2g:3(){5 a=j&&j.2j("."+h.H).1h(h.H);6 a&&a.7&&$.y(a[0],"2k")},2T:3(){B&&B.2z()},1p:3(){G&&G.3i()}}};$.2e.18=3(b,f){4(b!==1x){6 A.I(3(){4(A.2d){5 a=A.2d();4(f===1x||b==f){a.4n("2c",b);a.3h()}w{a.4m(D);a.4l("2c",b);a.4k("2c",f);a.3h()}}w 4(A.3g){A.3g(b,f)}w 4(A.1C){A.1C=b;A.3e=f}})}5 c=A[0];4(c.2d){5 e=1K.18.4j(),3d=c.F,2a="<->",2f=e.3b.7;e.3b=2a;5 d=c.F.2H(2a);c.F=3d;A.18(d,d+2f);6{1I:d,39:d+2f}}w 4(c.1C!==1x){6{1I:c.1C,39:c.3e}}}})(4i);',62,272,'|||function|if|var|return|length|||||||||||||||||||||||||else||data|active|this|list|false|true|width|value|element|ACTIVE|each|toLowerCase|val|result|Autocompleter|visible|case|null|addClass|break|multipleSeparator|css|target|moveSelect|typeof|max|url|hide||bind|onChange||trigger|trimWords|for|multiple|selection|autocomplete|offsetHeight|scrollTop|trim|preventDefault|populate|matchSubset|cacheLength|removeClass|add|size|search|hideResultsNow|lastWord|slice|flush|unbind|stopLoading|arguments|matchCase|term|extend|formatMatch|matchContains|undefined|highlight|formatResult|parse|string|selectionStart|scroll|scrollHeight|mouseDownOnSelect|autoFill|progress|start|delay|document|li|new|findValueCallback|setTimeout|clearTimeout|appendTo|formatItem|needsInit|defaults|push|continue|left|selectFirst|browser|selectCurrent|show|request|replace|split|unautocomplete||loadingClass||setOptions|minChars|teststring|flushCache|character|createTextRange|fn|textLength|selected|in|top|filter|ac_data|fillList|limitNumberOfItems|BACKSPACE|movePosition|PAGEDOWN|click|hideResults|LI|focus|nodeName|PAGEUP|body|COMMA|init|empty|pageDown|ESC|pageUp|next|RETURN|ol|nullData|indexOf|TAB|strong|gi|maxHeight|keyCode|DEL|padding|resultsClass|inputClass|parseInt|DOWN|emptyList|form|dataType|bgiframe|opera|UP|extraParams|prev|load|Select|||display|mustMatch|substring||end|receiveData|text|Cache|orig|selectionEnd|join|setSelectionRange|select|remove|right|height|style|msie|overflow|off|offset|current|attr|is|find|ac_even|html|innerHeight|clientHeight|parentNode|tagName|while|mouseup|mousedown|index|blur|toUpperCase|mouseover|ul|188|default|absolute|position|div|ac_over|substr|ac_odd|charAt|word|180|RegExp|100|switch|400|auto|ac_loading|ac_results||ac_input|keydown|keypress|success|submit||limit|150|name|port|abort|mode|ajax|Date|timestamp|200|map|jQuery|createRange|moveEnd|moveStart|collapse|move'.split('|'),0,{})) -------------------------------------------------------------------------------- /static/js/gist.js: -------------------------------------------------------------------------------- 1 | function L() { 2 | if (window.console && window.console.log) 3 | console.log.apply(console, arguments); 4 | } 5 | 6 | var Comments = (function() { 7 | var _comments; 8 | return { 9 | load_comments : function(comments) { 10 | _comments = comments; 11 | }, 12 | display_all : function() { 13 | $.each(_comments, function(i, comment) { 14 | Comments.display(comment); 15 | }); 16 | $('div.file-comments').fadeIn(400); 17 | $('div.gist-file').each(function(i,e) { 18 | if ($('div.comment', e).size() > 5) { 19 | $('div.comment-link-bottom:hidden', e).show(); 20 | } 21 | }); 22 | 23 | }, 24 | display : function(comment) { 25 | var container; 26 | if (comment.file) { 27 | $('div.file-comments').each(function(i, e) { 28 | if ($(e).attr('data-file') == comment.file) { 29 | container = $(e); 30 | } 31 | }); 32 | } else { 33 | container = $('div.file-comments').eq(0); 34 | } 35 | var c = $('
').attr('id', 'comment-' + comment.id).addClass( 36 | 'comment'); 37 | var m = $('

').addClass('metadata'); 38 | if (comment.user.gravatar_html) { 39 | $('').addClass('gravatar').html( 40 | comment.user.gravatar_html).appendTo(m); 41 | } 42 | var username = comment.user.name; 43 | if (!username) { 44 | username = comment.user.login; 45 | } 46 | $('').addClass('user').html(username).appendTo(m); 47 | $('').addClass('ago').html(comment.ago + ' ago').appendTo(m); 48 | /* 49 | * $('') .addClass('comment') .text("Reply to this") 50 | * .attr('data-file', comment.file) .attr('data-id', comment.id) 51 | * .appendTo(m); 52 | */ 53 | c.append(m); 54 | $('

').html(comment.comment).appendTo(c); 55 | $('
').addClass('clearer').appendTo(c); 56 | container.append(c); 57 | } 58 | } 59 | })(); 60 | 61 | $(function() { 62 | $.getJSON('comments', function(response) { 63 | if (response.error) { 64 | return alert(response.error) 65 | } 66 | Comments.load_comments(response.comments); 67 | Comments.display_all(); 68 | 69 | $('a.comment').each(function() { 70 | var self = $(this); 71 | self.qtip( { 72 | //id : 'comment_qtip', 73 | content : { 74 | text : $('#comment-outer .comment-inner'), 75 | title : { 76 | text : 'Comment', 77 | button : true 78 | } 79 | }, 80 | position : { 81 | my : 'center', 82 | at : 'center', 83 | target : $(window) 84 | }, 85 | show : { 86 | event : 'click', 87 | solo : true, 88 | modal : true 89 | }, 90 | events : { 91 | hide : function(event, api) { 92 | //$('#ui-tooltip-comment_qtip').remove(); 93 | //$('.qtip:hidden').remove(); 94 | } 95 | }, 96 | hide : false, 97 | style : 'ui-tooltip-light ui-tooltip-rounded' 98 | }); 99 | 100 | self.click(function() { 101 | $('textarea[name="comment"]') 102 | .keyup(_preview_comment_on_event); 103 | $('textarea[name="comment"]') 104 | .bind('change', _preview_comment_on_change); 105 | $('input[name="file"]').val(self.attr('data-file')); 106 | $('.about-file code').text(self.attr('data-file')); 107 | return false; 108 | }); 109 | 110 | }); 111 | }); 112 | 113 | if ($('input[name="tags"]').size()) { 114 | // set up autocomplete 115 | $.getJSON('/tags.json',function(r) { 116 | L(r.tags); 117 | $('input[name="tags"]').autocomplete(r.tags, { 118 | autoFill: false, 119 | multiple: true, 120 | multipleSeparator: ', '}); 121 | 122 | }); 123 | } 124 | 125 | $('a.love').click(function() { 126 | var f = $(this).parents('form'); 127 | $('span.love', f).fadeOut(200); 128 | var points_before = $('span.love', f).text(); 129 | $.post(f.attr('action'), f.serializeArray(), function(response) { 130 | if (response.not_logged_in) { 131 | $('a.love') 132 | .attr('href', $('#login-url').attr('href')) 133 | .unbind('click') 134 | .text("Please log in first. Takes about 4 seconds."); 135 | } else { 136 | $('span.love', f) 137 | .text(response.points) 138 | .fadeIn(600); 139 | $('a.love', f).hide(); 140 | $('a.unlove, strong.loved', f).show(600); 141 | } 142 | }); 143 | return false; 144 | }); 145 | 146 | $('a.unlove').click(function() { 147 | var f = $(this).parents('form'); 148 | $('span.love', f).fadeOut(200); 149 | var points_before = $('span.love', f).text(); 150 | $.post(f.attr('action'), f.serializeArray(), function(response) { 151 | if (response.not_logged_in) { 152 | $('a.unlove') 153 | .attr('href', $('#login-url').attr('href')) 154 | .unbind('click') 155 | .text("Please log in first. Takes about 4 seconds."); 156 | } else { 157 | $('span.love', f) 158 | .text(response.points) 159 | .fadeIn(600); 160 | $('a.unlove,strong.loved', f).hide(); 161 | $('a.love', f).show(600); 162 | 163 | } 164 | }); 165 | return false; 166 | 167 | }); 168 | 169 | }); 170 | 171 | function _preview_comment_on_change() { 172 | $('textarea[name="comment"]').unbind('change'); 173 | _preview_comment(function(err) { 174 | if (!err) { 175 | // reattach 176 | $('textarea[name="comment"]') 177 | .bind('change',_preview_comment_on_change); 178 | } 179 | }); 180 | } 181 | 182 | function _preview_comment_on_event(event) { 183 | if (event.keyCode === 13) { 184 | $('textarea[name="comment"]').unbind('keyup'); 185 | _preview_comment(function(err) { 186 | if (!err) { 187 | // reattach 188 | $('textarea[name="comment"]') 189 | .keyup(_preview_comment_on_event); 190 | } 191 | }); 192 | } 193 | } 194 | 195 | function _preview_comment(callback) { 196 | $.ajax('/preview_markdown', { 197 | cache : false, 198 | dataType : 'json', 199 | type : 'POST', 200 | data : { 201 | text : $('textarea[name="comment"]').val() 202 | }, 203 | success : function(res) { 204 | if (res.html) { 205 | if ($('.preview_markdown:hidden').size()) { 206 | $('.preview_markdown').show(); 207 | } 208 | $('._preview').html(res.html) 209 | } 210 | callback(null, res.html); 211 | } 212 | }); 213 | } 214 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils import * -------------------------------------------------------------------------------- /utils/decorators.py: -------------------------------------------------------------------------------- 1 | from tornado.web import HTTPError 2 | 3 | def login_required(func): 4 | def is_logged_in(self): 5 | guid = self.get_secure_cookie('user') 6 | if guid: 7 | if self.db.users.User(dict(guid=guid)): 8 | return func(self) 9 | raise HTTPError(403, "Must be logged in") 10 | return is_logged_in -------------------------------------------------------------------------------- /utils/git.py: -------------------------------------------------------------------------------- 1 | import os, re 2 | import logging 3 | from subprocess import Popen, PIPE 4 | 5 | def get_git_revision(): 6 | return _get_git_revision() 7 | 8 | def _get_git_revision(): 9 | # this is actually very fast. Takes about 0.01 seconds on my machine! 10 | home = os.path.dirname(__file__) 11 | proc = Popen('cd %s;git log --no-color -n 1 --date=iso' % home, 12 | shell=True, stdout=PIPE, stderr=PIPE) 13 | output = proc.communicate() 14 | try: 15 | date = [x.split('Date:')[1].split('+')[0].strip() for x in 16 | output[0].splitlines() if x.startswith('Date:')][0] 17 | date_wo_tz = re.split('-\d{4}', date)[0].strip() 18 | return date_wo_tz 19 | except IndexError: 20 | logging.debug("OUTPUT=%r" % output[0], exc_info=True) 21 | logging.debug("ERROR=%r" % output[1]) 22 | return 'unknown' 23 | -------------------------------------------------------------------------------- /utils/http_test_client.py: -------------------------------------------------------------------------------- 1 | from urllib import urlencode 2 | import Cookie 3 | from tornado.httpclient import HTTPRequest 4 | from tornado import escape 5 | 6 | __version__ = '1.2' 7 | 8 | class LoginError(Exception): 9 | pass 10 | 11 | class HTTPClientMixin(object): 12 | 13 | def get(self, url, data=None, headers=None, follow_redirects=False): 14 | if data is not None: 15 | if isinstance(data, dict): 16 | data = urlencode(data, True) 17 | if '?' in url: 18 | url += '&%s' % data 19 | else: 20 | url += '?%s' % data 21 | return self._fetch(url, 'GET', headers=headers, 22 | follow_redirects=follow_redirects) 23 | 24 | def post(self, url, data, headers=None, follow_redirects=False): 25 | if data is not None: 26 | if isinstance(data, dict): 27 | data = urlencode(data, True) 28 | return self._fetch(url, 'POST', data, headers, 29 | follow_redirects=follow_redirects) 30 | 31 | def _fetch(self, url, method, data=None, headers=None, follow_redirects=True): 32 | full_url = self.get_url(url) 33 | request = HTTPRequest(full_url, follow_redirects=follow_redirects, 34 | headers=headers, method=method, body=data) 35 | self.http_client.fetch(request, self.stop) 36 | return self.wait() 37 | 38 | 39 | class TestClient(HTTPClientMixin): 40 | def __init__(self, testcase): 41 | self.testcase = testcase 42 | self.cookies = Cookie.SimpleCookie() 43 | 44 | def _render_cookie_back(self): 45 | return ''.join(['%s=%s;' %(x, morsel.value) 46 | for (x, morsel) 47 | in self.cookies.items()]) 48 | 49 | def get(self, url, data=None, headers=None, follow_redirects=False): 50 | if self.cookies: 51 | if headers is None: 52 | headers = dict() 53 | headers['Cookie'] = self._render_cookie_back() 54 | response = self.testcase.get(url, data=data, headers=headers, 55 | follow_redirects=follow_redirects) 56 | 57 | self._update_cookies(response.headers) 58 | return response 59 | 60 | def post(self, url, data, headers=None, follow_redirects=False): 61 | if self.cookies: 62 | if headers is None: 63 | headers = dict() 64 | headers['Cookie'] = self._render_cookie_back() 65 | response = self.testcase.post(url, data=data, headers=headers, 66 | follow_redirects=follow_redirects) 67 | self._update_cookies(response.headers) 68 | return response 69 | 70 | def _update_cookies(self, headers): 71 | try: 72 | sc = headers['Set-Cookie'] 73 | self.cookies.update(Cookie.SimpleCookie( 74 | escape.native_str(sc))) 75 | except KeyError: 76 | return 77 | 78 | def login(self, email, password, url='/auth/login/'): 79 | data = dict(email=email, password=password) 80 | response = self.post(url, data, follow_redirects=False) 81 | if response.code != 302: 82 | raise LoginError(response.body) 83 | if 'Error' in response.body: 84 | raise LoginError(response.body) 85 | -------------------------------------------------------------------------------- /utils/routes.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | class route(object): 3 | """ 4 | decorates RequestHandlers and builds up a list of routables handlers 5 | 6 | Tech Notes (or 'What the *@# is really happening here?') 7 | -------------------------------------------------------- 8 | 9 | Everytime @route('...') is called, we instantiate a new route object which 10 | saves off the passed in URI. Then, since it's a decorator, the function is 11 | passed to the route.__call__ method as an argument. We save a reference to 12 | that handler with our uri in our class level routes list then return that 13 | class to be instantiated as normal. 14 | 15 | Later, we can call the classmethod route.get_routes to return that list of 16 | tuples which can be handed directly to the tornado.web.Application 17 | instantiation. 18 | 19 | Example 20 | ------- 21 | 22 | @route('/some/path') 23 | class SomeRequestHandler(RequestHandler): 24 | pass 25 | 26 | @route('/some/path', name='other') 27 | class SomeOtherRequestHandler(RequestHandler): 28 | pass 29 | 30 | my_routes = route.get_routes() 31 | """ 32 | _routes = [] 33 | 34 | def __init__(self, uri, name=None): 35 | self._uri = uri 36 | self.name = name 37 | 38 | def __call__(self, _handler): 39 | """gets called when we class decorate""" 40 | name = self.name and self.name or _handler.__name__ 41 | self._routes.append(tornado.web.url(self._uri, _handler, name=name)) 42 | return _handler 43 | 44 | @classmethod 45 | def get_routes(self): 46 | return self._routes 47 | 48 | def route_redirect(from_, to, name=None): 49 | route._routes.append(tornado.web.url(from_, tornado.web.RedirectHandler, dict(url=to), name=name)) 50 | -------------------------------------------------------------------------------- /utils/send_mail/__init__.py: -------------------------------------------------------------------------------- 1 | from send_email import send_email -------------------------------------------------------------------------------- /utils/send_mail/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /utils/send_mail/backends/base.py: -------------------------------------------------------------------------------- 1 | """Base email backend class.""" 2 | 3 | class BaseEmailBackend(object): 4 | """ 5 | Base class for email backend implementations. 6 | 7 | Subclasses must at least overwrite send_messages(). 8 | """ 9 | def __init__(self, fail_silently=False, **kwargs): 10 | self.fail_silently = fail_silently 11 | 12 | def open(self): 13 | """Open a network connection. 14 | 15 | This method can be overwritten by backend implementations to 16 | open a network connection. 17 | 18 | It's up to the backend implementation to track the status of 19 | a network connection if it's needed by the backend. 20 | 21 | This method can be called by applications to force a single 22 | network connection to be used when sending mails. See the 23 | send_messages() method of the SMTP backend for a reference 24 | implementation. 25 | 26 | The default implementation does nothing. 27 | """ 28 | pass 29 | 30 | def close(self): 31 | """Close a network connection.""" 32 | pass 33 | 34 | def send_messages(self, email_messages): 35 | """ 36 | Sends one or more EmailMessage objects and returns the number of email 37 | messages sent. 38 | """ 39 | raise NotImplementedError 40 | 41 | -------------------------------------------------------------------------------- /utils/send_mail/backends/console.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | 4 | from utils.send_mail.backends.base import BaseEmailBackend 5 | 6 | class EmailBackend(BaseEmailBackend): 7 | def __init__(self, *args, **kwargs): 8 | self.stream = kwargs.pop('stream', sys.stdout) 9 | self._lock = threading.RLock() 10 | super(EmailBackend, self).__init__(*args, **kwargs) 11 | 12 | def send_messages(self, email_messages): 13 | """Write all messages to the stream in a thread-safe way.""" 14 | if not email_messages: 15 | return 16 | self._lock.acquire() 17 | try: 18 | # The try-except is nested to allow for 19 | # Python 2.4 support (Refs #12147) 20 | try: 21 | stream_created = self.open() 22 | for message in email_messages: 23 | self.stream.write('%s\n' % message.message().as_string()) 24 | self.stream.write('-'*79) 25 | self.stream.write('\n') 26 | self.stream.flush() # flush after each message 27 | if stream_created: 28 | self.close() 29 | except: 30 | if not self.fail_silently: 31 | raise 32 | finally: 33 | self._lock.release() 34 | return len(email_messages) 35 | -------------------------------------------------------------------------------- /utils/send_mail/backends/locmem.py: -------------------------------------------------------------------------------- 1 | """ 2 | Backend for test environment. 3 | """ 4 | import sys 5 | #print sys.path 6 | import utils.send_mail as mail 7 | #from utils.send_mail import send_email as mail 8 | from utils.send_mail.backends.base import BaseEmailBackend 9 | 10 | class EmailBackend(BaseEmailBackend): 11 | """A email backend for use during test sessions. 12 | 13 | The test connection stores email messages in a dummy outbox, 14 | rather than sending them out on the wire. 15 | 16 | The dummy outbox is accessible through the outbox instance attribute. 17 | """ 18 | def __init__(self, *args, **kwargs): 19 | super(EmailBackend, self).__init__(*args, **kwargs) 20 | if not hasattr(mail, 'outbox'): 21 | mail.outbox = [] 22 | 23 | def send_messages(self, messages): 24 | """Redirect messages to the dummy outbox""" 25 | mail.outbox.extend(messages) 26 | return len(messages) 27 | -------------------------------------------------------------------------------- /utils/send_mail/backends/smtp.py: -------------------------------------------------------------------------------- 1 | """SMTP email backend class.""" 2 | 3 | import smtplib 4 | import socket 5 | import threading 6 | 7 | from utils.send_mail.backends.base import BaseEmailBackend 8 | from utils.send_mail.dns_name import DNS_NAME 9 | from utils.send_mail import config 10 | 11 | class EmailBackend(BaseEmailBackend): 12 | """ 13 | A wrapper that manages the SMTP network connection. 14 | """ 15 | def __init__(self, host=None, port=None, username=None, password=None, 16 | use_tls=None, fail_silently=False, **kwargs): 17 | super(EmailBackend, self).__init__(fail_silently=fail_silently) 18 | self.host = host or config.EMAIL_HOST 19 | self.port = port or config.EMAIL_PORT 20 | self.username = username or config.EMAIL_HOST_USER 21 | self.password = password or config.EMAIL_HOST_PASSWORD 22 | if use_tls is None: 23 | self.use_tls = config.EMAIL_USE_TLS 24 | else: 25 | self.use_tls = use_tls 26 | self.connection = None 27 | self._lock = threading.RLock() 28 | 29 | def open(self): 30 | """ 31 | Ensures we have a connection to the email server. Returns whether or 32 | not a new connection was required (True or False). 33 | """ 34 | if self.connection: 35 | # Nothing to do if the connection is already open. 36 | return False 37 | try: 38 | # If local_hostname is not specified, socket.getfqdn() gets used. 39 | # For performance, we use the cached FQDN for local_hostname. 40 | self.connection = smtplib.SMTP(self.host, self.port, 41 | local_hostname=DNS_NAME.get_fqdn()) 42 | if self.use_tls: 43 | self.connection.ehlo() 44 | self.connection.starttls() 45 | self.connection.ehlo() 46 | if self.username and self.password: 47 | self.connection.login(self.username, self.password) 48 | return True 49 | except: 50 | if not self.fail_silently: 51 | raise 52 | 53 | def close(self): 54 | """Closes the connection to the email server.""" 55 | try: 56 | try: 57 | self.connection.quit() 58 | except socket.sslerror: 59 | # This happens when calling quit() on a TLS connection 60 | # sometimes. 61 | self.connection.close() 62 | except: 63 | if self.fail_silently: 64 | return 65 | raise 66 | finally: 67 | self.connection = None 68 | 69 | def send_messages(self, email_messages): 70 | """ 71 | Sends one or more EmailMessage objects and returns the number of email 72 | messages sent. 73 | """ 74 | if not email_messages: 75 | return 76 | self._lock.acquire() 77 | try: 78 | new_conn_created = self.open() 79 | if not self.connection: 80 | # We failed silently on open(). 81 | # Trying to send would be pointless. 82 | return 83 | num_sent = 0 84 | for message in email_messages: 85 | sent = self._send(message) 86 | if sent: 87 | num_sent += 1 88 | if new_conn_created: 89 | self.close() 90 | finally: 91 | self._lock.release() 92 | return num_sent 93 | 94 | def _send(self, email_message): 95 | """A helper method that does the actual sending.""" 96 | if not email_message.recipients(): 97 | return False 98 | try: 99 | self.connection.sendmail(email_message.from_email, 100 | email_message.recipients(), 101 | email_message.message().as_string()) 102 | except: 103 | if not self.fail_silently: 104 | raise 105 | return False 106 | return True 107 | -------------------------------------------------------------------------------- /utils/send_mail/config.py: -------------------------------------------------------------------------------- 1 | EMAIL_HOST = 'mail.fry-it.com' 2 | EMAIL_PORT = 25 3 | 4 | EMAIL_HOST_USER = None 5 | EMAIL_HOST_PASSWORD = None 6 | 7 | EMAIL_USE_TLS = False -------------------------------------------------------------------------------- /utils/send_mail/dns_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | Email message and email sending related helper functions. 3 | """ 4 | 5 | import socket 6 | 7 | 8 | # Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of 9 | # seconds, which slows down the restart of the server. 10 | class CachedDnsName(object): 11 | def __str__(self): 12 | return self.get_fqdn() 13 | 14 | def get_fqdn(self): 15 | if not hasattr(self, '_fqdn'): 16 | self._fqdn = socket.getfqdn() 17 | return self._fqdn 18 | 19 | DNS_NAME = CachedDnsName() 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /utils/send_mail/importlib.py: -------------------------------------------------------------------------------- 1 | # Taken from Python 2.7 with permission from/by the original author. 2 | import sys 3 | 4 | def _resolve_name(name, package, level): 5 | """Return the absolute name of the module to be imported.""" 6 | if not hasattr(package, 'rindex'): 7 | raise ValueError("'package' not set to a string") 8 | dot = len(package) 9 | for x in xrange(level, 1, -1): 10 | try: 11 | dot = package.rindex('.', 0, dot) 12 | except ValueError: 13 | raise ValueError("attempted relative import beyond top-level " 14 | "package") 15 | return "%s.%s" % (package[:dot], name) 16 | 17 | 18 | def import_module(name, package=None): 19 | """Import a module. 20 | 21 | The 'package' argument is required when performing a relative import. It 22 | specifies the package to use as the anchor point from which to resolve the 23 | relative import to an absolute import. 24 | 25 | """ 26 | if name.startswith('.'): 27 | if not package: 28 | raise TypeError("relative imports require the 'package' argument") 29 | level = 0 30 | for character in name: 31 | if character != '.': 32 | break 33 | level += 1 34 | name = _resolve_name(name[level:], package, level) 35 | __import__(name) 36 | return sys.modules[name] 37 | -------------------------------------------------------------------------------- /utils/send_mail/send_email.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import os 4 | from email.Header import Header 5 | from email.MIMEText import MIMEText 6 | from email.Utils import formatdate 7 | 8 | from dns_name import DNS_NAME 9 | from importlib import import_module 10 | 11 | class BadHeaderError(ValueError): 12 | pass 13 | 14 | def force_unicode(s, encoding): 15 | if isinstance(s, unicode): 16 | return s 17 | return unicode(s, encoding) 18 | 19 | class Promise(object):pass 20 | def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): 21 | """ 22 | Returns a bytestring version of 's', encoded as specified in 'encoding'. 23 | 24 | If strings_only is True, don't convert (some) non-string-like objects. 25 | """ 26 | if strings_only and isinstance(s, (types.NoneType, int)): 27 | return s 28 | if isinstance(s, Promise): 29 | return unicode(s).encode(encoding, errors) 30 | elif not isinstance(s, basestring): 31 | try: 32 | return str(s) 33 | except UnicodeEncodeError: 34 | if isinstance(s, Exception): 35 | # An Exception subclass containing non-ASCII data that doesn't 36 | # know how to print itself properly. We shouldn't raise a 37 | # further exception. 38 | return ' '.join([smart_str(arg, encoding, strings_only, 39 | errors) for arg in s]) 40 | return unicode(s).encode(encoding, errors) 41 | elif isinstance(s, unicode): 42 | return s.encode(encoding, errors) 43 | elif s and encoding != 'utf-8': 44 | return s.decode('utf-8', errors).encode(encoding, errors) 45 | else: 46 | return s 47 | 48 | 49 | def make_msgid(idstring=None): 50 | """Returns a string suitable for RFC 2822 compliant Message-ID, e.g: 51 | 52 | <20020201195627.33539.96671@nightshade.la.mastaler.com> 53 | 54 | Optional idstring if given is a string used to strengthen the 55 | uniqueness of the message id. 56 | """ 57 | timeval = time.time() 58 | utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval)) 59 | try: 60 | pid = os.getpid() 61 | except AttributeError: 62 | # No getpid() in Jython, for example. 63 | pid = 1 64 | randint = random.randrange(100000) 65 | if idstring is None: 66 | idstring = '' 67 | else: 68 | idstring = '.' + idstring 69 | idhost = DNS_NAME 70 | msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) 71 | return msgid 72 | 73 | 74 | def forbid_multi_line_headers(name, val, encoding): 75 | """Forbids multi-line headers, to prevent header injection.""" 76 | encoding = encoding or 'utf-8' 77 | val = force_unicode(val, encoding) 78 | if '\n' in val or '\r' in val: 79 | raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name)) 80 | try: 81 | val = val.encode('ascii') 82 | except UnicodeEncodeError: 83 | if name.lower() in ('to', 'from', 'cc'): 84 | result = [] 85 | for nm, addr in getaddresses((val,)): 86 | nm = str(Header(nm.encode(encoding), encoding)) 87 | result.append(formataddr((nm, str(addr)))) 88 | val = ', '.join(result) 89 | else: 90 | val = Header(val.encode(encoding), encoding) 91 | else: 92 | if name.lower() == 'subject': 93 | val = Header(val) 94 | return name, val 95 | 96 | class SafeMIMEText(MIMEText): 97 | 98 | def __init__(self, text, subtype, charset): 99 | self.encoding = charset 100 | MIMEText.__init__(self, text, subtype, charset) 101 | 102 | def __setitem__(self, name, val): 103 | name, val = forbid_multi_line_headers(name, val, self.encoding) 104 | MIMEText.__setitem__(self, name, val) 105 | 106 | class EmailMessage(object): 107 | """ 108 | A container for email information. 109 | """ 110 | content_subtype = 'plain' 111 | mixed_subtype = 'mixed' 112 | encoding = None # None => use settings default 113 | 114 | def __init__(self, subject, body, from_email, to=None, bcc=None, 115 | connection=None, attachments=None, headers=None): 116 | """ 117 | Initialize a single email message (which can be sent to multiple 118 | recipients). 119 | 120 | All strings used to create the message can be unicode strings 121 | (or UTF-8 bytestrings). The SafeMIMEText class will handle any 122 | necessary encoding conversions. 123 | """ 124 | if to: 125 | assert not isinstance(to, basestring), '"to" argument must be a list or tuple' 126 | self.to = list(to) 127 | else: 128 | self.to = [] 129 | if bcc: 130 | assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple' 131 | self.bcc = list(bcc) 132 | else: 133 | self.bcc = [] 134 | self.from_email = from_email 135 | self.subject = subject 136 | self.body = body 137 | self.attachments = attachments or [] 138 | self.extra_headers = headers or {} 139 | self.connection = connection 140 | 141 | def get_connection(self, fail_silently=False): 142 | #from django.core.mail import get_connection 143 | if not self.connection: 144 | raise NotImplementedError 145 | #self.connection = get_connection(fail_silently=fail_silently) 146 | return self.connection 147 | 148 | def message(self): 149 | encoding = self.encoding or 'utf-8' 150 | 151 | msg = SafeMIMEText(smart_str(self.body, encoding), 152 | self.content_subtype, encoding) 153 | msg = self._create_message(msg) 154 | msg['Subject'] = self.subject 155 | msg['From'] = self.extra_headers.get('From', self.from_email) 156 | msg['To'] = ', '.join(self.to) 157 | 158 | # Email header names are case-insensitive (RFC 2045), so we have to 159 | # accommodate that when doing comparisons. 160 | header_names = [key.lower() for key in self.extra_headers] 161 | if 'date' not in header_names: 162 | msg['Date'] = formatdate() 163 | if 'message-id' not in header_names: 164 | msg['Message-ID'] = make_msgid() 165 | for name, value in self.extra_headers.items(): 166 | if name.lower() == 'from': # From is already handled 167 | continue 168 | msg[name] = value 169 | return msg 170 | 171 | def recipients(self): 172 | """ 173 | Returns a list of all recipients of the email (includes direct 174 | addressees as well as Bcc entries). 175 | """ 176 | return self.to + self.bcc 177 | 178 | def send(self, fail_silently=False): 179 | """Sends the email message.""" 180 | if not self.recipients(): 181 | # Don't bother creating the network connection if there's nobody to 182 | # send to. 183 | return 0 184 | return self.get_connection(fail_silently).send_messages([self]) 185 | 186 | def attach(self, filename=None, content=None, mimetype=None): 187 | """ 188 | Attaches a file with the given filename and content. The filename can 189 | be omitted and the mimetype is guessed, if not provided. 190 | 191 | If the first parameter is a MIMEBase subclass it is inserted directly 192 | into the resulting message attachments. 193 | """ 194 | if isinstance(filename, MIMEBase): 195 | assert content == mimetype == None 196 | self.attachments.append(filename) 197 | else: 198 | assert content is not None 199 | self.attachments.append((filename, content, mimetype)) 200 | 201 | def attach_file(self, path, mimetype=None): 202 | """Attaches a file from the filesystem.""" 203 | filename = os.path.basename(path) 204 | content = open(path, 'rb').read() 205 | self.attach(filename, content, mimetype) 206 | 207 | def _create_message(self, msg): 208 | return self._create_attachments(msg) 209 | 210 | def _create_attachments(self, msg): 211 | if self.attachments: 212 | encoding = self.encoding or settings.DEFAULT_CHARSET 213 | body_msg = msg 214 | msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding) 215 | if self.body: 216 | msg.attach(body_msg) 217 | for attachment in self.attachments: 218 | if isinstance(attachment, MIMEBase): 219 | msg.attach(attachment) 220 | else: 221 | msg.attach(self._create_attachment(*attachment)) 222 | return msg 223 | 224 | def _create_mime_attachment(self, content, mimetype): 225 | """ 226 | Converts the content, mimetype pair into a MIME attachment object. 227 | """ 228 | basetype, subtype = mimetype.split('/', 1) 229 | if basetype == 'text': 230 | encoding = self.encoding or settings.DEFAULT_CHARSET 231 | attachment = SafeMIMEText(smart_str(content, encoding), subtype, encoding) 232 | else: 233 | # Encode non-text attachments with base64. 234 | attachment = MIMEBase(basetype, subtype) 235 | attachment.set_payload(content) 236 | Encoders.encode_base64(attachment) 237 | return attachment 238 | 239 | def _create_attachment(self, filename, content, mimetype=None): 240 | """ 241 | Converts the filename, content, mimetype triple into a MIME attachment 242 | object. 243 | """ 244 | if mimetype is None: 245 | mimetype, _ = mimetypes.guess_type(filename) 246 | if mimetype is None: 247 | mimetype = DEFAULT_ATTACHMENT_MIME_TYPE 248 | attachment = self._create_mime_attachment(content, mimetype) 249 | if filename: 250 | attachment.add_header('Content-Disposition', 'attachment', 251 | filename=filename) 252 | return attachment 253 | 254 | 255 | def get_connection(backend, fail_silently=False, **kwds): 256 | """Load an e-mail backend and return an instance of it. 257 | 258 | Both fail_silently and other keyword arguments are used in the 259 | constructor of the backend. 260 | """ 261 | path = backend# or settings.EMAIL_BACKEND 262 | try: 263 | mod_name, klass_name = path.rsplit('.', 1) 264 | mod = import_module(mod_name) 265 | except ImportError, e: 266 | raise 267 | klass = getattr(mod, klass_name) 268 | return klass(fail_silently=fail_silently, **kwds) 269 | 270 | def send_email(backend, subject, message, from_email, recipient_list, 271 | fail_silently=False, auth_user=None, auth_password=None, 272 | connection=None, headers=None): 273 | """ 274 | Easy wrapper for sending a single message to a recipient list. All members 275 | of the recipient list will see the other recipients in the 'To' field. 276 | 277 | If auth_user is None, the EMAIL_HOST_USER setting is used. 278 | If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. 279 | 280 | Note: The API for this method is frozen. New code wanting to extend the 281 | functionality should use the EmailMessage class directly. 282 | """ 283 | connection = connection or get_connection(backend, 284 | username=auth_user, 285 | password=auth_password, 286 | fail_silently=fail_silently) 287 | return EmailMessage(subject, message, from_email, recipient_list, 288 | connection=connection, 289 | headers=headers).send() 290 | -------------------------------------------------------------------------------- /utils/timesince.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | # Language constants 4 | MINUTE = 'minute' 5 | MINUTES = 'minutes' 6 | HOUR = 'hour' 7 | HOURS = 'hours' 8 | YEAR = 'year' 9 | YEARS = 'years' 10 | MONTH = 'month' 11 | MONTHS = 'months' 12 | WEEK = 'week' 13 | WEEKS = 'weeks' 14 | DAY = 'day' 15 | DAYS = 'days' 16 | AND = 'and' 17 | 18 | 19 | #@register.filter 20 | def smartertimesince(d, now=None): 21 | if not isinstance(d, datetime.datetime): 22 | d = datetime.datetime(d.year, d.month, d.day) 23 | if now and not isinstance(now, datetime.datetime): 24 | now = datetime.datetime(now.year, now.month, now.day) 25 | 26 | if not now: 27 | if d.tzinfo: 28 | from django.utils.tzinfo import LocalTimezone 29 | now = datetime.datetime.now(LocalTimezone(d)) 30 | else: 31 | now = datetime.datetime.now() 32 | 33 | r = timeSince(d, now, max_no_sections=1, minute_granularity=True) 34 | if not r: 35 | return "seconds" 36 | return r 37 | 38 | # Copied and adopted from FriedZopeBase 39 | def timeSince(firstdate, seconddate, afterword=None, 40 | minute_granularity=False, 41 | max_no_sections=3): 42 | """ 43 | Use two date objects to return in plain english the difference between them. 44 | E.g. "3 years and 2 days" 45 | or "1 year and 3 months and 1 day" 46 | 47 | Try to use weeks when the no. of days > 7 48 | 49 | If less than 1 day, return number of hours. 50 | 51 | If there is "no difference" between them, return false. 52 | """ 53 | 54 | def wrap_afterword(result, afterword=afterword): 55 | if afterword is not None: 56 | return "%s %s" % (result, afterword) 57 | else: 58 | return result 59 | 60 | fdo = firstdate 61 | sdo = seconddate 62 | 63 | day_difference = abs(sdo-fdo).days 64 | 65 | years = day_difference/365 66 | months = (day_difference % 365)/30 67 | days = (day_difference % 365) % 30 68 | minutes = ((day_difference % 365) % 30) % 24 69 | 70 | 71 | if days == 0 and months == 0 and years == 0: 72 | # use hours 73 | hours = abs(sdo-fdo).seconds/3600 74 | if hours == 1: 75 | return wrap_afterword("1 %s" % (HOUR)) 76 | elif hours > 0: 77 | return wrap_afterword("%s %s" % (hours, HOURS)) 78 | elif minute_granularity: 79 | 80 | minutes = abs(sdo-fdo).seconds / 60 81 | if minutes == 1: 82 | return wrap_afterword("1 %s" % MINUTE) 83 | elif minutes > 0: 84 | return wrap_afterword("%s %s" % (minutes, MINUTES)) 85 | else: 86 | # if the differnce is smaller than 1 minute, 87 | # return 0. 88 | return 0 89 | else: 90 | # if the difference is smaller than 1 hour, 91 | # return it false 92 | return 0 93 | else: 94 | s = [] 95 | if years == 1: 96 | s.append('1 %s'%(YEAR)) 97 | elif years > 1: 98 | s.append('%s %s'%(years,YEARS)) 99 | 100 | if months == 1: 101 | s.append('1 %s'%MONTH) 102 | elif months > 1: 103 | s.append('%s %s'%(months,MONTHS)) 104 | 105 | if days == 1: 106 | s.append('1 %s'%DAY) 107 | elif days == 7: 108 | s.append('1 %s'%WEEK) 109 | elif days == 14: 110 | s.append('2 %s'%WEEKS) 111 | elif days == 21: 112 | s.append('3 %s'%WEEKS) 113 | elif days > 14: 114 | weeks = days / 7 115 | days = days % 7 116 | if weeks == 1: 117 | s.append('1 %s'%WEEK) 118 | else: 119 | s.append('%s %s'%(weeks, WEEKS)) 120 | if days % 7 == 1: 121 | s.append('1 %s'%DAY) 122 | elif days > 0: 123 | 124 | s.append('%s %s'%(days % 7,DAYS)) 125 | elif days > 1: 126 | s.append('%s %s'%(days,DAYS)) 127 | 128 | s = s[:max_no_sections] 129 | 130 | if len(s)>1: 131 | return wrap_afterword("%s" % (string.join(s,' %s '%AND))) 132 | else: 133 | return wrap_afterword("%s" % s[0]) 134 | 135 | 136 | -------------------------------------------------------------------------------- /utils/truncate.py: -------------------------------------------------------------------------------- 1 | def truncate_words(s, num, end_text='...'): 2 | """Truncates a string after a certain number of words. Takes an optional 3 | argument of what should be used to notify that the string has been 4 | truncated, defaults to ellipsis (...)""" 5 | length = int(num) 6 | words = s.split() 7 | if len(words) > length: 8 | words = words[:length] 9 | if not words[-1].endswith(end_text): 10 | words.append(end_text) 11 | return u' '.join(words) -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | 4 | def niceboolean(value): 5 | if type(value) is bool: 6 | return value 7 | falseness = ('','no','off','false','none','0', 'f') 8 | return str(value).lower().strip() not in falseness 9 | 10 | 11 | email_re = re.compile( 12 | r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom 13 | r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string 14 | r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE) # domain 15 | def valid_email(email): 16 | return bool(email_re.search(email)) 17 | 18 | 19 | 20 | from random import choice 21 | from string import letters 22 | def random_string(length): 23 | return ''.join(choice(letters) for i in xrange(length)) 24 | 25 | def gravatar_html(is_secure, gravatar_id, width_and_height=140): 26 | # this is how github does it 27 | if is_secure: 28 | src = 'https://secure.gravatar.com/avatar/%s' % gravatar_id 29 | else: 30 | src = 'http://gravatar.com/avatar/%s' % gravatar_id 31 | width = height = int(width_and_height) 32 | src += '?s=%s' % width 33 | src += '&d=https://d3nwyuy0nl342s.cloudfront.net%2Fimages%2Fgravat'\ 34 | 'ars%2Fgravatar-140.png' 35 | tmpl = '' 36 | return tmpl % (width, height, src) 37 | 38 | def html_quote(text): 39 | return text.replace('<','<').replace('>','>').replace('"','"') --------------------------------------------------------------------------------