# HG changeset patch # User Drew Perttula # Date 1361439541 28800 # Node ID f8c4c7ce5f4a0b5e21c39ef5baf5c7b2595237d0 # Parent 409da49c148db98d6279d1ca0b785608517ddf1f lots of href additions: add/edit, nav fixes Ignore-this: 863335c4680ac9bcc6a7fc5867638d61 diff -r 409da49c148d -r f8c4c7ce5f4a link.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/link.py Thu Feb 21 01:39:01 2013 -0800 @@ -0,0 +1,31 @@ + +class NotFound(ValueError): + pass + +class Links(object): + def __init__(self, db): + self.coll = db['links'] + + def insertOrUpdate(self, doc): + self.extract(doc) + self.coll.update({'href':doc['href']}, doc, upsert=True, safe=True) + + def extract(self, doc): + forUsers = [] + tags = [] + for t in doc.get('tag', '').split(' '): + if t.startswith('for:'): + forUsers.append(t[4:]) + else: + tags.append(t) + doc['extracted'] = dict(tags=tags, forUsers=forUsers) + + def find(self, uri): + docs = list(self.coll.find({'href': uri})) + if len(docs) == 0: + raise NotFound("not found") + elif len(docs) > 1: + raise ValueError("%s docs found for href %s" % (len(docs), uri)) + else: + return docs[0] + diff -r 409da49c148d -r f8c4c7ce5f4a lookup.py --- a/lookup.py Sun Feb 17 21:12:53 2013 -0800 +++ b/lookup.py Thu Feb 21 01:39:01 2013 -0800 @@ -5,16 +5,33 @@ /user /user/tag+tag+tag +and the add-bookmark stuff + """ -import pymongo, bottle, time, urllib, datetime +import pymongo, bottle, time, urllib, datetime, json, restkit from urllib2 import urlparse from dateutil.tz import tzlocal from bottle import static_file from jadestache import Renderer +from pagetitle import PageTitle +from link import Links, NotFound db = pymongo.Connection('bang', tz_aware=True)['href'] - +pageTitle = PageTitle(db) +links = Links(db) renderer = Renderer(search_dirs=['template'], debug=bottle.DEBUG) +siteRoot = 'https://bigasterisk.com/href' + +def getLoginBar(): + openidProxy = restkit.Resource("http://bang:9023/") + return openidProxy.get("_loginBar", + headers={"Cookie" : bottle.request.headers.get('cookie')}).body_string() + +def getUser(): + agent = bottle.request.headers.get('x-foaf-agent', None) + username = db['user'].find_one({'_id':agent})['username'] if agent else None + return username, agent + @bottle.route('/static/') def server_static(filename): return static_file(filename, root='static') @@ -35,6 +52,7 @@ doc['tagWords'] = [{'word' : w} for w in doc['tag'].split(None)] doc['domain'] = urlparse.urlparse(doc['href']).netloc + doc['editLink'] = 'addLink?' + urllib.urlencode([('url', doc['href'])]) out['links'].append(doc) out['stats'] = {'queryTimeMs' : round((time.time() - t1) * 1000, 2)} @@ -46,15 +64,17 @@ dt = (time.time() - t1) * 1000 rendered = rendered.replace('TEMPLATETIME', "%.02f ms" % dt) return rendered - -def getUser(): - return 'drewpca' # logged in user @bottle.route('/addLink') def addLink(): - out = {'toRoot': '.'} - out['user'] = getUser() - out['withKnockout'] = True + out = { + 'toRoot': '.', + 'absRoot': siteRoot, + 'user': getUser()[0], + 'withKnockout': True, + 'fillHrefJson': json.dumps(bottle.request.params.get('url', '')), + 'loginBar': getLoginBar(), + } return renderWithTime('add.jade', out) @bottle.route('/addOverlay') @@ -63,8 +83,27 @@ return "" + +@bottle.route('/addLink/proposedUri') +def proposedUri(): + uri = bottle.request.params.uri + user, _ = getUser() - proposal check existing links, get page title (stuff that in db), get tags from us and other serviecs. maybe the deferred ones ater + try: + prevDoc = links.find(uri) + except NotFound: + prevDoc = None + + return { + 'description': prevDoc['description'] if prevDoc else pageTitle.pageTitle(uri), + 'tag' : prevDoc['tag'] if prevDoc else '', + 'extended' : prevDoc['extended'] if prevDoc else '', + 'suggestedTags':['tag1', 'tag2'], + 'existed':prevDoc is not None, + } + +if 0: + pass#proposal check existing links, get page title (stuff that in db), get tags from us and other serviecs. maybe the deferred ones ater @bottle.route('//') @@ -75,6 +114,7 @@ def userAll(user): data = recentTags(user, tags=None) + data['loginBar'] = getLoginBar() data['desc'] = "%s's recent links" % user data['toRoot'] = "." data['stats']['template'] = 'TEMPLATETIME' @@ -83,7 +123,9 @@ @bottle.route('/', method='POST') def userAddLink(user): p = bottle.request.params - doc = dict( + if getUser()[0] != user: + raise ValueError("not logged in as %s" % user) + links.insertOrUpdate(dict( user=user, description=p.description, extended=p.extended, @@ -92,21 +134,31 @@ #shared ?? tag=p.tag, t=datetime.datetime.now(tzlocal()), - ) - db['links'].insert(doc, safe=True) + )) - bottle.redirect(user) + bottle.redirect(siteRoot + '/' + user) @bottle.route('//') def userLinks(user, tags): tags = tags.split('+') data = recentTags(user, tags) + data['loginBar'] = getLoginBar() data['desc'] = "%s's recent links tagged %s" % (user, tags) data['toRoot'] = ".." data['pageTags'] = [{"word":t} for t in tags] data['stats']['template'] = 'TEMPLATETIME' return renderWithTime('links.jade', data) + +@bottle.route('/') +def root(): + data = { + 'loginBar': getLoginBar(), + 'toRoot': ".", + 'stats': {'template': 'TEMPLATETIME'}, + 'users': [{'user':doc['username']} for doc in db['user'].find()], + } + return renderWithTime('index.jade', data) if __name__ == '__main__': bottle.run(server='gunicorn', host='0.0.0.0', port=10002, workers=4) diff -r 409da49c148d -r f8c4c7ce5f4a pagetitle.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pagetitle.py Thu Feb 21 01:39:01 2013 -0800 @@ -0,0 +1,41 @@ +import lxml.html.soupparser +import datetime, socket +from dateutil.tz import tzlocal +import restkit + +class CantGetTitle(ValueError): + pass + +class PageTitle(object): + def __init__(self, db): + self.coll = db['pageTitle'] + + def getPageTitleNow(self, uri): + try: + response = restkit.request(uri, timeout=1, follow_redirect=True, + headers={ + 'user-agent': + 'link title checker - drewp@bigasterisk.com' + }) + if not response.status.startswith('2'): + raise CantGetTitle("(got %s)" % response.status) + root = lxml.html.soupparser.fromstring( + response.body_string()) + + for title in root.cssselect("title"): + return title.text + except restkit.RequestError: + raise CantGetTitle("(error requesting title from site)") + + def pageTitle(self, uri): + """page title from our db or by getting a new load from the page""" + doc = self.coll.find_one({'_id' : uri}) + if doc is None: + try: + title = self.getPageTitleNow(uri) + except CantGetTitle, e: + return str(e) + doc = {'_id': uri, 'title' : title, + 'getTime':datetime.datetime.now(tzlocal())} + self.coll.insert(doc, safe=True) + return doc['title'] diff -r 409da49c148d -r f8c4c7ce5f4a readme --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/readme Thu Feb 21 01:39:01 2013 -0800 @@ -0,0 +1,25 @@ +Mission: + +http://twitter.com/#!/diveintomark/status/15772480610242561 +You know what you should do with your life? Write a del.icio.us clone that no one will use and then die alone. + +todo: +ok openid logins +- autopaging, like my search UI uses +- autocomplete tag filter and autocomplete on post-new-link ui +- add counts to the autocomplete +- show list of common tags +- export common tags for use on homepage; match delicious api +- show narrowing tags and counts when a filter is going +- 100% ajax page refreshes +- some boingy graph that looks cool +- pull tags from delicious and other services at post time +- sync updates back to a delicious account (and others?) +- rdf export +- link edit UI +- 'send this link to an email addr now' ui, saving the history (so i can sort by most-forwarded, etc) +- pick up links from twitter +- maybe pick up links from played podcasts +- revyu on any link, get review data back so they can be used for sorting + + diff -r 409da49c148d -r f8c4c7ce5f4a static/add.js --- a/static/add.js Sun Feb 17 21:12:53 2013 -0800 +++ b/static/add.js Thu Feb 21 01:39:01 2013 -0800 @@ -1,5 +1,9 @@ var model = { - href: ko.observable("starter"), + href: ko.observable(""), + description: ko.observable(""), + tag: ko.observable(""), + extended: ko.observable(""), + submitLabel: ko.observable("Add"), }; ko.computed(function() { @@ -7,8 +11,15 @@ return; } - $.getJSON("proposal", {uri: model.href()}, function (data) { + $.getJSON("addLink/proposedUri", {uri: model.href()}, function (data) { + // these could arrive after the user has started typing in the fields! + + model.description(data.description); + model.tag(data.tag); + model.extended(data.extended); + model.submitLabel(data.existed ? "Update existing" : "Add"); + }); }); diff -r 409da49c148d -r f8c4c7ce5f4a static/gui.js --- a/static/gui.js Sun Feb 17 21:12:53 2013 -0800 +++ b/static/gui.js Thu Feb 21 01:39:01 2013 -0800 @@ -1,32 +1,47 @@ $("#filterTag").focus(); -function toggleTag(tag) { +function currentFilter() { var p = window.location.pathname; var comps = p.split("/"); - var selected = (comps[2] || "").split("+"); - var add = true; - var others = []; - for (var i=0; i < selected.length; i++) { - if (!selected[i]) { - continue; - } - if (selected[i] == tag) { - add = false; - } else { - others.push(selected[i]); - } + if (toRoot == ".") { + return []; + } else { + return (comps[comps.length-1] || "").split("+"); + } +} + +function toggleTag(tag) { + var selected = currentFilter(); + + if (selected.indexOf(tag) == -1) { + selected.push(tag); + } else { + selected.splice(selected.indexOf(tag), 1); } - if (add) { - others.push(tag); + + var newPath = window.location.pathname; + if (toRoot == ".") { + newPath += "/"; + } else { + newPath = newPath.replace( + /(.*\/)[^\/]*$/, "$1") } - window.location.pathname = "/" + comps[1] + "/" + others.join("+"); + console.log("user root", newPath); + if (selected.length) { + newPath += selected.join("+") + } else { + newPath = newPath.substr(0, newPath.length - 1); + } + console.log("done", newPath); + + window.location.pathname = newPath; } function backspaceLastTag() { var p = window.location.pathname; var comps = p.split("/"); - var selected = (comps[2] || "").split("+"); + var selected = (comps[comps.length-1] || "").split("+"); if (selected.length == 0) { return; } diff -r 409da49c148d -r f8c4c7ce5f4a static/style.css --- a/static/style.css Sun Feb 17 21:12:53 2013 -0800 +++ b/static/style.css Thu Feb 21 01:39:01 2013 -0800 @@ -53,4 +53,8 @@ .modified { margin-left: 24px; color: #5E6A6F; +} +input { + width: 500px; + padding: 4px; } \ No newline at end of file diff -r 409da49c148d -r f8c4c7ce5f4a template/add.jade.mustache --- a/template/add.jade.mustache Sun Feb 17 21:12:53 2013 -0800 +++ b/template/add.jade.mustache Thu Feb 21 01:39:01 2013 -0800 @@ -4,9 +4,19 @@ title add link {{> head.jade}} body + | {{{loginBar}}} + h2 add link - p Bookmarklet to get this form over any page: + p Bookmarklet to jump to the add page from a given page: + a(href="javascript:(function(){ + window.location.href = '{{absRoot}}/addLink?url='+encodeURIComponent(window.location.href)+ + '&title='+encodeURIComponent(document.title)+ + '&selected='+encodeURIComponent(''+( + window.getSelection ? window.getSelection(): document.getSelection? document.getSelection(): document.selection.createRange().text)); + })()") addpage + + p Bookmarklet to get this form over any page (unfinished): a(href="javascript:(function(){ var s=document.createElement('div'); s.innerHTML='Loading...'; @@ -23,7 +33,7 @@ s=document.createElement('script'); s.setAttribute('type','text/javascript'); - s.setAttribute('src','http://plus:10002/addOverlay'+ + s.setAttribute('src','{{absRoot}}/addOverlay'+ '?url='+encodeURIComponent(window.location.href)+ '&title='+encodeURIComponent(document.title)+ '&selected='+encodeURIComponent(''+( @@ -36,16 +46,18 @@ div URI: input(type='text', name='href', data-bind='value: href') div Title: - input(type='text', name='description') - div Tags: - input(type='text', name='tag') + input(type='text', name='description', data-bind='value: description') + div Tags (space-separated): + input(type='text', name='tag', data-bind='value: tag') div Comment: - textarea(name='extended') + textarea(name='extended', data-bind='value: extended') div Private: ... div Shared: ..? div - button(type='submit') - | Add + button(type='submit', data-bind="text: submitLabel") + Add {{> tail.jade}} script(src="{{toRoot}}/static/add.js") + script + model.href({{{fillHrefJson}}}) diff -r 409da49c148d -r f8c4c7ce5f4a template/index.jade.mustache --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/template/index.jade.mustache Thu Feb 21 01:39:01 2013 -0800 @@ -0,0 +1,17 @@ +doctype html +html + head + title href + {{> head.jade}} + body + | {{{loginBar}}} + h2 Users + + {{#users}} + p + a(href="{{user}}") {{user}} + {{/users}} + + p {{stats}} + + {{> tail.jade}} diff -r 409da49c148d -r f8c4c7ce5f4a template/links.jade.mustache --- a/template/links.jade.mustache Sun Feb 17 21:12:53 2013 -0800 +++ b/template/links.jade.mustache Thu Feb 21 01:39:01 2013 -0800 @@ -4,6 +4,7 @@ title {{desc}} {{> head.jade}} body + | {{{loginBar}}} h2 Showing {{desc}} p @@ -28,16 +29,19 @@ .tags | Tags: {{#tagWords}} - a.tag(href="{{word}}") {{word}} + a.tag(href="{{toRoot}}/tag/{{word}}", title="add/remove this tag from the current filter") {{word}} | {{/tagWords}} .hrefShown a(href="{{href}}") {{href}} .modified | Modified: {{t}} + .edit + a(href="{{toRoot}}/{{editLink}}") Edit {{/links}} p {{stats}} - + script + var toRoot = "{{toRoot}}"; {{> tail.jade}} diff -r 409da49c148d -r f8c4c7ce5f4a test_pagetitle.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test_pagetitle.py Thu Feb 21 01:39:01 2013 -0800 @@ -0,0 +1,9 @@ +import unittest +from pagetitle import PageTitle + +class TestPageTitle(unittest.TestCase): + def testLoads(self): + p = PageTitle({'pageTitle':None}) + t = p.getPageTitle("http://bigasterisk.com/") + self.assertEqual(t, "Drew Perttula") +