├── tests ├── __init__.py └── test_json.py ├── Procfile ├── nofussbm ├── static │ ├── robots.txt │ ├── favicon.ico │ ├── flattr-badge-large.png │ ├── forkme_right_darkblue_121621.png │ ├── jquery.cookie.js │ ├── html5.js │ ├── jquery.ba-bbq.min.js │ ├── bootstrap-modal.js │ ├── jquery.tablesorter.min.js │ └── bootstrap.min.css ├── debug.py ├── templates │ ├── list-content.html │ ├── uibase.html │ ├── base.html │ ├── options.html │ ├── list.html │ └── signup.html ├── helpers.py ├── json.py ├── tags.py ├── __init__.py └── api.py ├── .slugignore ├── extension ├── icon128.png ├── icon16.png ├── icon19.png ├── icon48.png ├── manifest.json ├── getinfo.js ├── options.html ├── options.js ├── popup.html └── popup.js ├── scripts ├── setenv ├── getenv ├── mongo ├── mongodump ├── logs ├── json_diff └── cli_client ├── .github └── FUNDING.yml ├── requirements.txt ├── .hgignore ├── wrapper.sh ├── .gitignore ├── README.md ├── CHANGELOG.txt └── LICENSE.txt /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./wrapper.sh 2 | 3 | -------------------------------------------------------------------------------- /nofussbm/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /.slugignore: -------------------------------------------------------------------------------- 1 | extension 2 | scripts 3 | README.rst 4 | LICENSE.txt 5 | CHANGELOG.txt 6 | -------------------------------------------------------------------------------- /extension/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapio/nofussbm/HEAD/extension/icon128.png -------------------------------------------------------------------------------- /extension/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapio/nofussbm/HEAD/extension/icon16.png -------------------------------------------------------------------------------- /extension/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapio/nofussbm/HEAD/extension/icon19.png -------------------------------------------------------------------------------- /extension/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapio/nofussbm/HEAD/extension/icon48.png -------------------------------------------------------------------------------- /scripts/setenv: -------------------------------------------------------------------------------- 1 | eval $(sed 's/\(.*\)=\(.*\)/export \1="\2"/' < ./.env) 2 | export PYTHONPATH=. -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mapio] 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | Flask-PyMongo==2.1.0 3 | gunicorn==19.9.0 4 | pymongo==3.7.2 5 | -------------------------------------------------------------------------------- /nofussbm/debug.py: -------------------------------------------------------------------------------- 1 | from . import app 2 | 3 | def _run(): 4 | app.run( debug = True, use_reloader = False ) -------------------------------------------------------------------------------- /nofussbm/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapio/nofussbm/HEAD/nofussbm/static/favicon.ico -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.log 4 | *.pem 5 | *.json 6 | .env* 7 | packed 8 | local 9 | TODO.txt 10 | .git 11 | -------------------------------------------------------------------------------- /nofussbm/static/flattr-badge-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapio/nofussbm/HEAD/nofussbm/static/flattr-badge-large.png -------------------------------------------------------------------------------- /nofussbm/static/forkme_right_darkblue_121621.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapio/nofussbm/HEAD/nofussbm/static/forkme_right_darkblue_121621.png -------------------------------------------------------------------------------- /scripts/getenv: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | heroku config --app nofussbm -s | egrep '(MONGOLAB_URI|SECRET_KEY|SENDGRID_PASSWORD|SENDGRID_USERNAME)' > .env 4 | -------------------------------------------------------------------------------- /scripts/mongo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os import environ 4 | from urlparse import urlparse 5 | 6 | up = urlparse( environ['MONGOLAB_URI'] ) 7 | 8 | print 'mongo -u {} -p {} {}:{}{}'.format( up.username, up.password, up.hostname, up.port, up.path ) 9 | -------------------------------------------------------------------------------- /nofussbm/templates/list-content.html: -------------------------------------------------------------------------------- 1 | {% for bm in bookmarks %} 2 |
  • 3 | {{ bm[ 2 ] }} ({{ bm[ 0 ] }}) [{{ bm[ 4 ] }}] 4 | 5 |
    {% for tag in bm[ 3 ] %}{{ tag }} {% endfor %}
    6 |
  • 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # trap TERM and change to QUIT 4 | trap 'echo wrapper killing $PID; kill -QUIT $PID' TERM 5 | 6 | # program to run 7 | gunicorn nofussbm:app --access-logfile=- --error-logfile=- -w 3 -b 0.0.0.0:$PORT & 8 | 9 | # capture PID and wait 10 | PID=$! 11 | echo wrapper started $PID 12 | wait 13 | -------------------------------------------------------------------------------- /scripts/mongodump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os import environ 4 | from urlparse import urlparse 5 | 6 | up = urlparse( environ['MONGOLAB_URI'] ) 7 | 8 | cmd = 'mongodump -u {} -p {} -h {}:{} -d {} --out local/dump'.format( up.username, up.password, up.hostname, up.port, up.path[1:] ) 9 | print run( cmd, shell = True ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Flask stuff: 7 | instance/ 8 | .webassets-cache 9 | 10 | # pyenv 11 | .python-version 12 | 13 | # dotenv 14 | .env 15 | 16 | # virtualenv 17 | venv/ 18 | ENV/ 19 | 20 | # PyCharm folder 21 | .idea/ 22 | 23 | *.log 24 | *.pem 25 | *.json 26 | packed 27 | dump 28 | -------------------------------------------------------------------------------- /scripts/logs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from sys import argv 4 | from nofussbm import mongo 5 | 6 | db = DB( 'MONGOLAB_URI' ) 7 | 8 | if len( argv ) > 1 and argv[ 1 ] == '-e': 9 | for entry in db[ 'error-logs' ].find(): 10 | print entry[ 'timestamp' ].as_datetime(), entry[ 'message' ] 11 | else: 12 | for entry in db[ 'access-logs' ].find(): 13 | print entry[ 'message' ] 14 | -------------------------------------------------------------------------------- /scripts/json_diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from json import load, dumps 4 | from sys import argv 5 | 6 | with ( open( argv[ 1 ], 'r' ) ) as f: 7 | a = load( f ) 8 | 9 | with ( open( argv[ 2 ], 'r' ) ) as f: 10 | b = load( f ) 11 | 12 | d = [] 13 | for x, y in zip( a, b ): 14 | if x != y: d.append( x ) 15 | 16 | print dumps( d, indent = 4, sort_keys = True, ensure_ascii = False ) -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "No Fuss Bookmarks", 3 | "manifest_version": 2, 4 | "version": "1.4.1", 5 | "description": "An extension to post bookmarks on nofussbm.", 6 | "browser_action": { 7 | "default_icon": "icon19.png", 8 | "default_title": "No Fuss Bookmarks", 9 | "default_popup": "popup.html" 10 | }, 11 | "icons": { 12 | "16": "icon16.png", 13 | "48": "icon48.png", 14 | "128": "icon128.png" 15 | }, 16 | "content_scripts": [ 17 | { 18 | "matches": [ "http://*/*", "https://*/*" ], 19 | "js": [ "getinfo.js" ] 20 | } 21 | ], 22 | "permissions": [ 23 | "tabs", 24 | "http://*/" 25 | ], 26 | "options_page": "options.html" 27 | } 28 | -------------------------------------------------------------------------------- /scripts/cli_client: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z $URL ]; then 4 | URL='https://nofussbm.herokuapp.com/api/v1/' 5 | fi 6 | 7 | METHOD=$1 8 | ARG=$2 9 | case $METHOD in 10 | GET|get) 11 | if [ -z $ARG ]; then 12 | curl $SHOW_HEADERS -H "X-Nofussbm-Key: $NOFUSSBM_KEY" $URL 13 | else 14 | curl $SHOW_HEADERS -H "X-Nofussbm-Key: $NOFUSSBM_KEY" ${URL}$ARG 15 | fi ;; 16 | QUERY|query) 17 | curl $SHOW_HEADERS -H "X-Nofussbm-Key: $NOFUSSBM_KEY" -H "X-Nofussbm-Query: $ARG" $URL ;; 18 | DELETE|delete|del) 19 | curl $SHOW_HEADERS -d @- -X DELETE -H "Content-type: application/json" -H "X-Nofussbm-Key: $NOFUSSBM_KEY" $URL ;; 20 | POST|post) 21 | curl $SHOW_HEADERS -d @- -X POST -H "Content-type: application/json" -H "X-Nofussbm-Key: $NOFUSSBM_KEY" $URL ;; 22 | PUT|put) 23 | curl $SHOW_HEADERS -d @- -X PUT -H "Content-type: application/json" -H "X-Nofussbm-Key: $NOFUSSBM_KEY" $URL ;; 24 | esac 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | No Fuss Bookmarks 2 | ================= 3 | 4 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/d/cgahojkildjmnkcjpfbojdbnpbphdieo.svg)](https://chrome.google.com/webstore/detail/no-fuss-bookmarks/cgahojkildjmnkcjpfbojdbnpbphdieo) 5 | 6 | No Fuss Bookmarks is a very simple **software** and **service** to store 7 | bookmarks especially designed for hackers (that don't need fancy interfaces, 8 | but **nice API**). 9 | 10 | The software is a simple RESTful server, written using 11 | [Flask](http://flask.pocoo.org/), providing a CRUD interface to a 12 | [mongoDB](http://www.mongodb.org/) store, plus a very basic Google Chrome 13 | extension to submit bookmarks. 14 | 15 | The service is just an incarnation of such software hosted by 16 | [heroku](http://www.heroku.com/) and [mongolab](http://mongolab.com) that you 17 | can freely use just submitting your email below to obtain your API key. 18 | 19 | For more details, please visit the [service signup 20 | page](http://nofussbm.herokuapp.com/signup.html). 21 | -------------------------------------------------------------------------------- /extension/getinfo.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, Massimo Santini 3 | 4 | This file is part of "No Fuss Bookmarks". 5 | 6 | "No Fuss Bookmarks" is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by the Free 8 | Software Foundation, either version 3 of the License, or (at your option) any 9 | later version. 10 | 11 | "No Fuss Bookmarks" is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU General Public License along with 17 | "No Fuss Bookmarks". If not, see . 18 | */ 19 | 20 | chrome.extension.onRequest.addListener( 21 | function( request, sender, sendResponse ) { 22 | sendResponse( { 23 | "title": document.title, 24 | "selection": window.getSelection().toString() 25 | } ); 26 | } ); 27 | -------------------------------------------------------------------------------- /nofussbm/templates/uibase.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | 11 | {% endblock %} 12 | 13 | {% block body %} 14 |
    15 |
    16 |
    17 | No Fuss Bookmarks 18 | 22 |

    [options]

    23 |
    24 |
    25 |
    26 | 27 |
    28 | 29 | 32 | 33 |
    34 | {% block content %}{% endblock %} 35 |
    36 | 37 |
    38 | {% endblock %} -------------------------------------------------------------------------------- /extension/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nofussbm options 6 | 7 | 8 | 9 |
    10 | Enter API Key:
    11 |
    12 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /extension/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, Massimo Santini 3 | 4 | This file is part of "No Fuss Bookmarks". 5 | 6 | "No Fuss Bookmarks" is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by the Free 8 | Software Foundation, either version 3 of the License, or (at your option) any 9 | later version. 10 | 11 | "No Fuss Bookmarks" is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU General Public License along with 17 | "No Fuss Bookmarks". If not, see . 18 | */ 19 | 20 | var form; 21 | 22 | function onload() { 23 | form = document.forms.options; 24 | form.key.value = localStorage.getItem( 'key' ); 25 | } 26 | 27 | function set() { 28 | localStorage.setItem( 'key', form.key.value ); 29 | } 30 | 31 | document.addEventListener( 'DOMContentLoaded', function () { 32 | onload(); 33 | document.getElementById( 'key' ).addEventListener( 'change', set ); 34 | } ); 35 | -------------------------------------------------------------------------------- /extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nofussbm popup 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | Result: 14 |
    15 | 16 | 17 | 35 | -------------------------------------------------------------------------------- /nofussbm/static/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Cookie plugin 3 | * 4 | * Copyright (c) 2010 Klaus Hartl (stilbuero.de) 5 | * Dual licensed under the MIT and GPL licenses: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * http://www.gnu.org/licenses/gpl.html 8 | * 9 | */ 10 | jQuery.cookie = function (key, value, options) { 11 | 12 | // key and at least value given, set cookie... 13 | if (arguments.length > 1 && String(value) !== "[object Object]") { 14 | options = jQuery.extend({}, options); 15 | 16 | if (value === null || value === undefined) { 17 | options.expires = -1; 18 | } 19 | 20 | if (typeof options.expires === 'number') { 21 | var days = options.expires, t = options.expires = new Date(); 22 | t.setDate(t.getDate() + days); 23 | } 24 | 25 | value = String(value); 26 | 27 | return (document.cookie = [ 28 | encodeURIComponent(key), '=', 29 | options.raw ? value : encodeURIComponent(value), 30 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 31 | options.path ? '; path=' + options.path : '', 32 | options.domain ? '; domain=' + options.domain : '', 33 | options.secure ? '; secure' : '' 34 | ].join('')); 35 | } 36 | 37 | // key and possibly options given, get cookie... 38 | options = value || {}; 39 | var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent; 40 | return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null; 41 | }; 42 | -------------------------------------------------------------------------------- /nofussbm/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011, Massimo Santini 2 | # 3 | # This file is part of "No Fuss Bookmarks". 4 | # 5 | # "No Fuss Bookmarks" is free software: you can redistribute it and/or modify it 6 | # under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) any 8 | # later version. 9 | # 10 | # "No Fuss Bookmarks" is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along with 16 | # "No Fuss Bookmarks". If not, see . 17 | 18 | from email.mime.text import MIMEText 19 | from smtplib import SMTP 20 | 21 | from bson.objectid import ObjectId, InvalidId 22 | 23 | from . import Config 24 | 25 | 26 | def to_id( id_as_str ): 27 | res = None 28 | try: 29 | res = ObjectId( id_as_str ) 30 | except InvalidId: 31 | pass 32 | return res 33 | 34 | def query_from_dict( email, dct ): 35 | query = { 'email': email } 36 | if not dct: return query 37 | if 'id' in dct: 38 | query[ '_id' ] = ObjectId( dct[ 'id'] ) 39 | if 'tags' in dct: 40 | tags = map( lambda _: _.strip(), dct[ 'tags' ].split( ',' ) ) 41 | query[ 'tags' ] = { '$all': tags } 42 | if 'title' in dct: 43 | query[ 'title' ] = { '$regex': dct[ 'title' ], '$options': 'i' } 44 | return query 45 | 46 | 47 | # Utility functions 48 | 49 | def send_mail( frm, to, subject, body ): 50 | msg = MIMEText( body.encode( 'utf8' ), 'plain', 'utf8' ) 51 | msg[ 'Subject' ] = subject 52 | msg[ 'From' ] = frm 53 | msg[ 'To' ] = to 54 | s = SMTP( 'smtp.sendgrid.net' ) 55 | s.login( Config.SENDGRID_USERNAME, Config.SENDGRID_PASSWORD ) 56 | s.sendmail( frm, [ to ], msg.as_string() ) 57 | s.quit() 58 | -------------------------------------------------------------------------------- /nofussbm/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | No Fuss Bookmarks 6 | 7 | 8 | 11 | 12 | 13 | {% block head %}{% endblock %} 14 | 24 | 25 | 26 | {% block body %}{% endblock %} 27 | 28 | 29 | 47 | -------------------------------------------------------------------------------- /nofussbm/static/html5.js: -------------------------------------------------------------------------------- 1 | // iepp v2.1pre @jon_neal & @aFarkas github.com/aFarkas/iepp 2 | // html5shiv @rem remysharp.com/html5-enabling-script 3 | // Dual licensed under the MIT or GPL Version 2 licenses 4 | /*@cc_on(function(a,b){function r(a){var b=-1;while(++b 3 | 4 | This file is part of 'No Fuss Bookmarks'. 5 | 6 | 'No Fuss Bookmarks' is free software: you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by the Free 8 | Software Foundation, either version 3 of the License, or (at your option) any 9 | later version. 10 | 11 | 'No Fuss Bookmarks' is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 14 | details. 15 | 16 | You should have received a copy of the GNU General Public License along with 17 | 'No Fuss Bookmarks'. If not, see . 18 | */ 19 | 20 | var form, key; 21 | 22 | function onload() { 23 | form = document.forms.bookmarks; 24 | key = localStorage.getItem( 'key' ); 25 | if ( ! key ) { 26 | alert( "Please set your API key in extension's options" ); 27 | } 28 | chrome.tabs.getSelected( null, function( tab ) { 29 | chrome.tabs.sendRequest( tab.id, {}, function handler( response ) { 30 | form.url.value = tab.url; 31 | form.title.value = response.title; 32 | } ); 33 | } ); 34 | } 35 | 36 | function postUrl() { 37 | var data = JSON.stringify( [ { 'url': form.url.value, 'title': form.title.value, 'tags': form.tags.value } ] ); 38 | var req = new XMLHttpRequest(); 39 | req.onreadystatechange = function() { 40 | if( this.readyState == 4 && this.status == 200 ) { 41 | response = JSON.parse( req.responseText ); 42 | if ( response[ 'added' ].length ) { 43 | form.status.value = 'Added Bookmark, id = ' + response[ 'added' ][ 0 ]; 44 | } else { 45 | form.status.value = 'The bookmark was not added'; 46 | } 47 | } else if ( this.readyState == 4 && this.status != 200 ) { 48 | form.status.value = 'An error has occurred'; 49 | } 50 | }; 51 | req.open( 'POST', 'http://nofussbm.herokuapp.com/api/v1/', true ); 52 | req.setRequestHeader( 'Content-Type', 'application/json' ); 53 | req.setRequestHeader( 'X-Nofussbm-Key', key ); 54 | req.send( data ); 55 | console.log( data ); 56 | } 57 | 58 | document.addEventListener( 'DOMContentLoaded', function () { 59 | onload(); 60 | document.getElementById( 'postit' ).addEventListener( 'click', postUrl ); 61 | } ); 62 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import nofussbm 3 | import flask 4 | import datetime 5 | 6 | from bson import ObjectId 7 | 8 | 9 | class NofussbmJSONTestCase( unittest.TestCase ): 10 | 11 | def setUp( self ): 12 | self.app = nofussbm.app 13 | self.app.config[ 'TESTING' ] = True 14 | self.client = nofussbm.app.test_client() 15 | 16 | def tearDown( self ): 17 | pass 18 | 19 | def testJson( self ): 20 | expected_load = [ { 21 | u'date-modified': datetime.datetime(2016, 7, 16, 0, 0, 43, 237000), 22 | u'title': u'Google', 23 | u'url': u'https://google.com', 24 | u'tags': [ u'google', u'search engine' ], 25 | u'id': ObjectId( '5789792b19f4cb77cc3be929' ), 26 | u'date-added': datetime.datetime(2016, 7, 16, 0, 0, 43, 237000) 27 | } ] 28 | to_load = ( '[{' 29 | '"date-added": "2016-07-16 00:00:43.237000", ' 30 | '"date-modified": "2016-07-16 00:00:43.237000", ' 31 | '"id": "5789792b19f4cb77cc3be929", ' 32 | '"tags": "google,search engine", ' 33 | '"title": "Google", ' 34 | '"url": "https://google.com"' 35 | '}]' ) 36 | expected_dump = ( '[{' 37 | '"date-added": "2016-07-16 00:00:43.237000", ' 38 | '"date-modified": "2016-07-16 00:00:43.237000", ' 39 | '"id": "5789792b19f4cb77cc3be929", ' 40 | '"tags": ["google", "search engine"], ' 41 | '"title": "Google", ' 42 | '"url": "https://google.com"' 43 | '}]' ) 44 | 45 | self.assertEqual( self.app.json_encoder, nofussbm.json.NofussbmJSONEncoder ) 46 | self.assertEqual( self.app.json_decoder, nofussbm.json.NofussbmJSONDecoder ) 47 | 48 | with self.app.app_context(): 49 | load = flask.json.loads( to_load ) # expected_dump 50 | dump = flask.json.dumps( expected_load ) 51 | self.assertEqual( load, expected_load ) 52 | self.assertEqual( dump, expected_dump ) 53 | 54 | tags_as_list_load = flask.json.loads( expected_dump ) 55 | self.assertEqual( tags_as_list_load, expected_load ) 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 2011-12-12 Massimo Santini 2 | 3 | * server: added options management and filter on html listing (with ajax navigation) 4 | 5 | 2011-12-11 Massimo Santini 6 | 7 | * scripts: refactored the way we connect to the db, or manipulate the URI 8 | 9 | 2011-12-10 Massimo Santini 10 | 11 | * server: Moved APIs to a (Flask) blueprint 12 | * server: Added some support for tags statistics (map reduce mongo stuff) 13 | 14 | 2011-12-06 Massimo Santini 15 | 16 | * server: Added (very initial) support for html listing, just add ?html= to the / URL 17 | 18 | 2011-11-29 Massimo Santini 19 | 20 | * server: Added range and query support for REST GET view 21 | * server: Added 'skip' and 'limit' query arguments to text list 22 | * server: The text list is now sorted, added stats (user/#bookmarks), '/stats' URL is now reserver (un-aliasable) 23 | * server: Alias addition/update is now handled with an atomic mongodb operation 24 | * server: The 'date-modified' is set to 'date-added' if absent, databse have been updated accordingly 25 | * scripts: Added a script to connect to mongolab via mongo cli 26 | 27 | 2011-11-27 Massimo Santini 28 | 29 | * scripts: Added a script to connect to mongolab (via pymongo) and dump databases 30 | * server: Added ref to official extension on Chrome Web Store in signup page 31 | 32 | 2011-11-26 Massimo Santini 33 | 34 | * server: Added alias support, as suggested by Matteo 35 | * extension: Cosmetic fix (icons...) 36 | * server: INCOMPATIBLE CHANGE: Removed /list/, as suggested by Matteo 37 | * server: Removed GZipMW 38 | 39 | 2011-11-25 Massimo Santini 40 | 41 | * scripts: Added some heroku helper scripts 42 | * Restructuring repo so that it can be directly used with heroku 43 | * Added GPL 44 | 45 | 2011-11-24 Massimo Santini 46 | 47 | * server: Streamlined tags and title search 48 | * server: Added Delicious import 49 | * server: BUGFIX: delete and update are based on id _and_ email 50 | * server: Incoming json is cleaned-up (by removing all but some fileds) 51 | * server: fixed docs and added legalese 52 | 53 | 2011-11-23 Massimo Santini 54 | 55 | * server: Added a signup page, tag search, PUT and DELETE, fixed list view 56 | 57 | 2011-11-22 Massimo Santini 58 | 59 | * extension: first experiments -------------------------------------------------------------------------------- /nofussbm/json.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011, Massimo Santini 2 | # 3 | # This file is part of "No Fuss Bookmarks". 4 | # 5 | # "No Fuss Bookmarks" is free software: you can redistribute it and/or modify it 6 | # under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) any 8 | # later version. 9 | # 10 | # "No Fuss Bookmarks" is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along with 16 | # "No Fuss Bookmarks". If not, see . 17 | 18 | from flask.json import JSONEncoder, JSONDecoder 19 | from datetime import datetime 20 | from bson.objectid import ObjectId 21 | from .helpers import to_id 22 | 23 | DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S.%f' 24 | 25 | 26 | class NofussbmJSONEncoder( JSONEncoder ): 27 | 28 | def default( self, obj ): 29 | if isinstance( obj, datetime ): 30 | return datetime.strftime( obj, DATETIME_FORMAT ) 31 | if isinstance( obj, ObjectId ): 32 | return str( obj ) 33 | return JSONEncoder.default( self, obj ) 34 | 35 | 36 | class NofussbmJSONDecoder( JSONDecoder ): 37 | 38 | def __init__( self, *args, **kwargs ): 39 | self.ALLOWED_KEYS = set( [ 'title', 'url', 'id', 'tags', 'date-added', 'date-modified' ] ) 40 | self.orig_object_hook = kwargs.pop( "object_hook", None ) 41 | super( NofussbmJSONDecoder, self ).__init__( *args, object_hook=self.custom_object_hook, **kwargs ) 42 | 43 | def custom_object_hook( self, dct ): 44 | res = dict() 45 | for key, value in dct.items(): 46 | if key not in self.ALLOWED_KEYS: 47 | continue 48 | if key == 'id': 49 | res[ 'id' ] = to_id( value ) 50 | elif key == 'tags': 51 | try: 52 | res[ 'tags' ] = [ _.strip() for _ in value.split( ',' ) ] 53 | except AttributeError: 54 | res[ 'tags' ] = [ _.strip() for _ in value] 55 | elif key.startswith( 'date-' ): 56 | try: 57 | res[ key ] = datetime.strptime( value, DATETIME_FORMAT ) 58 | except: 59 | pass 60 | else: 61 | res[ key ] = value 62 | return res 63 | -------------------------------------------------------------------------------- /nofussbm/templates/options.html: -------------------------------------------------------------------------------- 1 | {% extends "uibase.html" %} 2 | 3 | {% block head %} 4 | 5 | 34 | {% endblock %} 35 | 36 | {% block content %} 37 | 40 | 41 |
    42 |
    43 | 44 | List page 45 | 46 |
    47 | 48 |
      49 |
    • 53 |
    • 57 |
    If set to "Text" you'll need to come back manually to this page to have the list in HTML again 58 |
    59 |
    60 | 61 |
    62 | 63 |
    64 | 65 | Set to 0 to show all your bookmarks in a single page 66 |
    67 |
    68 | 69 |
    70 | 71 |
      72 |
    • 76 | Computing the top tags list (and frequencies) can slow down page access 77 |
    • 78 |
    79 |
    80 | 81 |
    82 |
    83 | {% endblock %} 84 | -------------------------------------------------------------------------------- /nofussbm/tags.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011, Massimo Santini 2 | # 3 | # This file is part of "No Fuss Bookmarks". 4 | # 5 | # "No Fuss Bookmarks" is free software: you can redistribute it and/or modify it 6 | # under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) any 8 | # later version. 9 | # 10 | # "No Fuss Bookmarks" is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along with 16 | # "No Fuss Bookmarks". If not, see . 17 | 18 | from datetime import datetime 19 | 20 | from bson.code import Code 21 | from bson.son import SON 22 | 23 | def tags( db, email ): 24 | try: 25 | tags_updated = db.tags.find_one( { '_id.email': email }, { 'value.modified': 1 } )[ 'value' ][ 'modified' ] 26 | except TypeError: 27 | tags_updated = None 28 | try: 29 | bookmarks_updated = db.bookmarks.find_one( { 'email': email }, { 'date-modified': 1 }, sort = [ ('date-modified', -1 )] )[ 'date-modified'] 30 | except TypeError: 31 | bookmarks_updated = None 32 | if not tags_updated or tags_updated < bookmarks_updated: _update_tags( db, email ) 33 | return db.tags.find_one( { '_id.email': email } )[ 'value' ][ 'tags' ] 34 | 35 | def _update_tags( db, email ): 36 | 37 | db[ 'tags-exapnded' ].remove( { '_id.email': email } ) 38 | db.bookmarks.map_reduce( Code( """ 39 | function() { 40 | for ( index in this.tags ) { 41 | emit( { 'email': this.email, 'tag': this.tags[ index ] }, { count: 1, modified: this[ 'date-modified' ] } ); 42 | } 43 | } 44 | """ ), Code( """ 45 | function( key, values ) { 46 | var result = { count: 0, modified: values[ 0 ].modified }; 47 | values.forEach( function( value ) { 48 | result.count += value.count; 49 | if ( result.modified < value.modified ) result.modified = value.modified; 50 | } ); 51 | return result; 52 | } 53 | """ ), query = { 'email': email }, out = SON( [ ( 'merge', 'tags-exapnded' ) ] ) ) 54 | db[ 'tags-exapnded' ].map_reduce( Code (""" 55 | function() { 56 | emit( { 'email': this._id.email }, { 'tags': [ [ this._id.tag, this.value.count ] ], 'modified': this.value.modified } ); 57 | } 58 | """), Code( """ 59 | function( key, values ) { 60 | var result = { 'tags': [], 'modified': values[ 0 ].modified } 61 | values.forEach( function( value ) { 62 | result.tags.push.apply( result.tags, value.tags ); 63 | if ( result.modified < value.modified ) result.modified = value.modified; 64 | } ); 65 | return result; 66 | } 67 | """ ), finalize = Code( """ 68 | function( key, value ) { 69 | value.tags = value.tags.sort( function( a, b ) { return b[ 1 ] - a[ 1 ]; } ); 70 | return value; 71 | } 72 | """ ), query = { '_id.email': email }, out = SON( [ ( 'merge', 'tags' ) ] ) ) 73 | 74 | 75 | -------------------------------------------------------------------------------- /nofussbm/static/jquery.ba-bbq.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010 3 | * http://benalman.com/projects/jquery-bbq-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this); -------------------------------------------------------------------------------- /nofussbm/templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends "uibase.html" %} 2 | 3 | {% block head %} 4 | 9 | 10 | 11 | 63 | {% endblock %} 64 | 65 | {% block sidebar %} 66 |

    Filter

    67 | 68 |
    69 |
    70 | 71 |
    72 | 73 | 74 |
    75 |
    76 |
    77 | 78 |
    79 | 80 | 81 |
    82 |
    83 |
    84 | 85 |
    86 | 87 |
    88 |
    89 |
    90 | 91 | {% if top_tags %} 92 |

    Top tags

    93 |

    94 | {% for tag, count in top_tags[ : 100 ] %} 95 | {{ tag }} {{ count|int }} 96 | {% endfor %} 97 |

    98 | {% endif %} 99 | {% endblock %} 100 | 101 | {% block content %} 102 | 105 |
      106 |
      {% include "list-content.html" %}
      107 |
    108 | {% endblock %} 109 | -------------------------------------------------------------------------------- /nofussbm/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011, Massimo Santini 2 | # 3 | # This file is part of "No Fuss Bookmarks". 4 | # 5 | # "No Fuss Bookmarks" is free software: you can redistribute it and/or modify it 6 | # under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation, either version 3 of the License, or (at your option) any 8 | # later version. 9 | # 10 | # "No Fuss Bookmarks" is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 | # details. 14 | # 15 | # You should have received a copy of the GNU General Public License along with 16 | # "No Fuss Bookmarks". If not, see . 17 | 18 | from logging import StreamHandler, Formatter, DEBUG 19 | from os import environ 20 | 21 | from flask import Flask, make_response, request, redirect, url_for, abort, render_template 22 | from flask_pymongo import PyMongo 23 | 24 | from pymongo.errors import OperationFailure 25 | 26 | # Configure from the environment, global (immutable) variables (before submodules import) 27 | 28 | class Config( object ): 29 | SECRET_KEY = environ[ 'SECRET_KEY' ] 30 | SENDGRID_USERNAME = environ[ 'SENDGRID_USERNAME' ] 31 | SENDGRID_PASSWORD = environ[ 'SENDGRID_PASSWORD' ] 32 | MONGO_URI = environ[ 'MONGOLAB_URI' ] 33 | 34 | # Create the app and mongo helper 35 | app = Flask( __name__ ) 36 | 37 | app.config[ 'MONGO_URI' ] = Config.MONGO_URI 38 | app.config[ 'MONGO_CONNECT' ] = 'False' # http://api.mongodb.org/python/current/faq.html#using-pymongo-with-multiprocessing 39 | mongo = PyMongo( app ) 40 | 41 | from .api import api 42 | from .helpers import query_from_dict 43 | from .tags import tags 44 | from .json import NofussbmJSONEncoder, NofussbmJSONDecoder 45 | app.json_encoder = NofussbmJSONEncoder 46 | app.json_decoder = NofussbmJSONDecoder 47 | 48 | # Register APIs blueprint and setup {before,teardown}_request 49 | 50 | app.register_blueprint( api, url_prefix = '/api/v1' ) 51 | 52 | # @app.before_request 53 | # def before_request(): 54 | # g.db = DB( 'MONGOLAB_URI' ) 55 | 56 | # @app.teardown_request 57 | # def teardown_request( exception ): 58 | # del g.db 59 | 60 | # Log to stderr (so heroku logs will pick'em up) 61 | 62 | stderr_handler = StreamHandler() 63 | stderr_handler.setLevel( DEBUG ) 64 | stderr_handler.setFormatter( Formatter( '%(asctime)s [%(process)s] [%(levelname)s] [Flask: %(name)s] %(message)s', '%Y-%m-%d %H:%M:%S' ) ) 65 | app.logger.addHandler( stderr_handler ) 66 | app.logger.setLevel( DEBUG ) 67 | 68 | 69 | # Helpers 70 | 71 | def textify( text, code = 200 ): 72 | response = make_response( text + '\n', code ) 73 | response.headers[ 'Content-Type' ] = 'text/plain; charset=UTF-8' 74 | return response 75 | 76 | def ident2email( ident ): 77 | if '@' in ident: email = ident 78 | else: 79 | try: 80 | alias = mongo.db.aliases.find_one( { 'alias': ident }, { 'email': 1 } ) 81 | email = alias[ 'email' ] 82 | except TypeError: 83 | abort( 404 ) 84 | except OperationFailure: 85 | abort( 500 ) 86 | return email 87 | 88 | def list_query( email, limit = None ): 89 | args = request.args 90 | query = query_from_dict( email, args ) 91 | if 'skip' in args: 92 | skip = int( args[ 'skip' ] ) 93 | else: 94 | skip = 0 95 | if 'limit' in args: 96 | limit = int( args[ 'limit' ] ) 97 | else: 98 | if limit is None: limit = 0 99 | if skip < 0 or limit < 0: abort( 400 ) 100 | return mongo.db.bookmarks.find( query, skip = skip, limit = limit ).sort( [ ( 'date-modified', -1 ) ] ) 101 | 102 | 103 | # Public "views" 104 | 105 | @app.route( '/' ) 106 | def index(): 107 | return redirect( url_for( 'signup' ) ) 108 | 109 | @app.route( '/favicon.ico' ) 110 | def favicon(): 111 | return redirect( url_for( 'static', filename = 'favicon.ico' ) ) 112 | 113 | @app.route( '/robots.txt' ) 114 | def robots(): 115 | return redirect( url_for( 'static', filename = 'robots.txt' ) ) 116 | 117 | @app.route( '/signup.html' ) 118 | def signup(): 119 | return render_template( 'signup.html' ) 120 | 121 | @app.route( '/options.html' ) 122 | def options(): 123 | return render_template( 'options.html' ) 124 | 125 | @app.route( '/' ) 126 | def list( ident ): 127 | 128 | list_appearance = 'html' if request.headers[ 'User-Agent'].split( '/' )[ 0 ] in ( 'Microsoft Internet Explorer', 'Mozilla', 'Opera' ) else 'text' 129 | la_c = request.cookies.get( 'list_appearance' ) 130 | if la_c: list_appearance = la_c 131 | bpp_c = request.cookies.get( 'bookmarks_per_page' ) 132 | bookmarks_per_page = int( bpp_c ) if bpp_c else 10 133 | show_tags = request.cookies.get( 'show_tags' ) != 'false' 134 | content_only = 'content_only' in request.args 135 | 136 | result = [] 137 | email = ident2email( ident ) 138 | try: 139 | if list_appearance == 'html': 140 | for bm in list_query( email, bookmarks_per_page ): 141 | date = bm[ 'date-modified' ] 142 | result.append( ( date.strftime( '%Y-%m-%d' ), bm[ 'url' ], bm[ 'title' ], bm[ 'tags' ], bm[ '_id' ] ) ) 143 | if content_only: 144 | return render_template( 'list-content.html', bookmarks = result ) 145 | else: 146 | return render_template( 'list.html', bookmarks = result, top_tags = tags( mongo.db, email ) if show_tags else None ) 147 | else: 148 | for bm in list_query( email ): 149 | date = bm[ 'date-modified' ] 150 | result.append( u'\t'.join( ( date.strftime( '%Y-%m-%d' ), bm[ 'url' ], bm[ 'title' ], u','.join( bm[ 'tags' ] ) ) ) ) 151 | return textify( u'\n'.join( result ) ) 152 | except OperationFailure: 153 | abort( 500 ) 154 | -------------------------------------------------------------------------------- /nofussbm/static/bootstrap-modal.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-modal.js v1.4.0 3 | * http://twitter.github.com/bootstrap/javascript.html#modal 4 | * ========================================================= 5 | * Copyright 2011 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) 26 | * ======================================================= */ 27 | 28 | var transitionEnd 29 | 30 | $(document).ready(function () { 31 | 32 | $.support.transition = (function () { 33 | var thisBody = document.body || document.documentElement 34 | , thisStyle = thisBody.style 35 | , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined 36 | return support 37 | })() 38 | 39 | // set CSS transition event type 40 | if ( $.support.transition ) { 41 | transitionEnd = "TransitionEnd" 42 | if ( $.browser.webkit ) { 43 | transitionEnd = "webkitTransitionEnd" 44 | } else if ( $.browser.mozilla ) { 45 | transitionEnd = "transitionend" 46 | } else if ( $.browser.opera ) { 47 | transitionEnd = "oTransitionEnd" 48 | } 49 | } 50 | 51 | }) 52 | 53 | 54 | /* MODAL PUBLIC CLASS DEFINITION 55 | * ============================= */ 56 | 57 | var Modal = function ( content, options ) { 58 | this.settings = $.extend({}, $.fn.modal.defaults, options) 59 | this.$element = $(content) 60 | .delegate('.close', 'click.modal', $.proxy(this.hide, this)) 61 | 62 | if ( this.settings.show ) { 63 | this.show() 64 | } 65 | 66 | return this 67 | } 68 | 69 | Modal.prototype = { 70 | 71 | toggle: function () { 72 | return this[!this.isShown ? 'show' : 'hide']() 73 | } 74 | 75 | , show: function () { 76 | var that = this 77 | this.isShown = true 78 | this.$element.trigger('show') 79 | 80 | escape.call(this) 81 | backdrop.call(this, function () { 82 | var transition = $.support.transition && that.$element.hasClass('fade') 83 | 84 | that.$element 85 | .appendTo(document.body) 86 | .show() 87 | 88 | if (transition) { 89 | that.$element[0].offsetWidth // force reflow 90 | } 91 | 92 | that.$element.addClass('in') 93 | 94 | transition ? 95 | that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) : 96 | that.$element.trigger('shown') 97 | 98 | }) 99 | 100 | return this 101 | } 102 | 103 | , hide: function (e) { 104 | e && e.preventDefault() 105 | 106 | if ( !this.isShown ) { 107 | return this 108 | } 109 | 110 | var that = this 111 | this.isShown = false 112 | 113 | escape.call(this) 114 | 115 | this.$element 116 | .trigger('hide') 117 | .removeClass('in') 118 | 119 | $.support.transition && this.$element.hasClass('fade') ? 120 | hideWithTransition.call(this) : 121 | hideModal.call(this) 122 | 123 | return this 124 | } 125 | 126 | } 127 | 128 | 129 | /* MODAL PRIVATE METHODS 130 | * ===================== */ 131 | 132 | function hideWithTransition() { 133 | // firefox drops transitionEnd events :{o 134 | var that = this 135 | , timeout = setTimeout(function () { 136 | that.$element.unbind(transitionEnd) 137 | hideModal.call(that) 138 | }, 500) 139 | 140 | this.$element.one(transitionEnd, function () { 141 | clearTimeout(timeout) 142 | hideModal.call(that) 143 | }) 144 | } 145 | 146 | function hideModal (that) { 147 | this.$element 148 | .hide() 149 | .trigger('hidden') 150 | 151 | backdrop.call(this) 152 | } 153 | 154 | function backdrop ( callback ) { 155 | var that = this 156 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 157 | if ( this.isShown && this.settings.backdrop ) { 158 | var doAnimate = $.support.transition && animate 159 | 160 | this.$backdrop = $('