changeset 5:f8c4c7ce5f4a

lots of href additions: add/edit, nav fixes Ignore-this: 863335c4680ac9bcc6a7fc5867638d61
author Drew Perttula <drewp@bigasterisk.com>
date Thu, 21 Feb 2013 01:39:01 -0800
parents 409da49c148d
children ce779bdd2fb3
files link.py lookup.py pagetitle.py readme static/add.js static/gui.js static/style.css template/add.jade.mustache template/index.jade.mustache template/links.jade.mustache test_pagetitle.py
diffstat 11 files changed, 263 insertions(+), 42 deletions(-) [+]
line wrap: on
line diff
--- /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]
+            
--- 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/<filename>')
 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('/<user>/')
@@ -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('/<user>', 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('/<user>/<tags>')
 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)
--- /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']
--- /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
+
+
--- 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");
+        
     });
     
 });
--- 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;
     }
--- 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
--- 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}}})
--- /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}}
--- 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}}
--- /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")
+