changeset 10:e054949143e9

reworking addlink and shareWith support Ignore-this: b1665b776f3964f7fde219acadc51f32
author drewp@bigasterisk.com
date Fri, 15 Mar 2013 00:29:53 -0700
parents 7df920c18c83
children c26da994a43a
files link.py lookup.py static/add.js static/gui.js static/links.js static/style.css template/add.jade.mustache template/bookmarklets.jade.mustache template/head.jade.mustache template/index.jade.mustache template/links.jade.mustache template/tail.jade.mustache
diffstat 12 files changed, 295 insertions(+), 174 deletions(-) [+]
line wrap: on
line diff
--- a/link.py	Wed Mar 06 23:23:18 2013 -0800
+++ b/link.py	Fri Mar 15 00:29:53 2013 -0700
@@ -1,3 +1,5 @@
+import urlparse, urllib
+from dateutil.tz import tzlocal
 
 class NotFound(ValueError):
     pass
@@ -7,6 +9,8 @@
         self.coll = db['links']
         
     def insertOrUpdate(self, doc):
+        if not doc['href']:
+            raise ValueError("no link")
         self.extract(doc)
         self.coll.update({'href':doc['href']}, doc, upsert=True, safe=True)
 
@@ -29,3 +33,32 @@
         else:
             return docs[0]
             
+    def forDisplay(self, doc):
+        """return a mustache-ready dict for this db doc"""
+        out = doc.copy()
+        del out['_id']
+        out['t'] = out['t'].astimezone(tzlocal()).isoformat()
+        if not out['description'].strip():
+            out['displayDescription'] = out['href']
+        else:
+            out['displayDescription'] = out['description']
+
+        out['tagWords'] = [{'word' : w} for w in out['tag'].split(None)]
+        out['domain'] = urlparse.urlparse(out['href']).netloc
+        out['editLink'] = 'addLink?' + urllib.urlencode([('url', out['href'])])
+        out['shareWith'] = [{'label' : uri} for uri in doc.get('shareWith', [])]
+        return out
+
+    def fromPostdata(self, data, user, t):
+        if not user or not data.href:
+            raise ValueError("incomplete")
+        return dict(
+            user=user,
+            description=data.description,
+            extended=data.extended,
+            href=data.href,
+            private=data.private,
+            shareWith=filter(None, data.shareWith.split(',')),
+            tag=data.tag,
+            t=t,
+        )
--- a/lookup.py	Wed Mar 06 23:23:18 2013 -0800
+++ b/lookup.py	Fri Mar 15 00:29:53 2013 -0700
@@ -44,25 +44,20 @@
     if tags:
         spec['extracted.tags'] = {'$all' : tags}
     for doc in db['links'].find(spec, sort=[('t', -1)], limit=50):
-        del doc['_id']
-        doc['t'] = doc['t'].astimezone(tzlocal()).isoformat()
-        if not doc['description'].strip():
-            doc['displayDescription'] = doc['href']
-        else:
-            doc['displayDescription'] = doc['description']
-
-        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['links'].append(links.forDisplay(doc))
     out['stats'] = {'queryTimeMs' : round((time.time() - t1) * 1000, 2)}
     return out
 
-def allTags(user):
+def allTags(user, withTags=[]):
+    """withTags limits results to other tags that have been used with
+    those tags"""
+    withTags = set(withTags)
     count = defaultdict(lambda: 0) # tag : count
     for doc in db['links'].find({'user':user}, fields=['extracted.tags']):
-        for t in doc.get('extracted', {}).get('tags', []):
+        docTags = set(doc.get('extracted', {}).get('tags', []))
+        if withTags and not withTags.issubset(docTags):
+            continue
+        for t in docTags.difference(withTags):
             count[t] = count[t] + 1
     byFreq = [(n, t) for t,n in count.iteritems()]
     byFreq.sort(key=lambda (n,t): (-n, t))
@@ -108,13 +103,24 @@
         '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,
+        'shareWith' : prevDoc.get('shareWith', []) 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('/tags')
+def tagFilterComplete():
+    params = bottle.request.params
+    haveTags = filter(None, params['have'].split(','))
+    return {'tags' : [
+        {'id': t['label'],
+         'text': "%s (%s%s)" % (t['label'], t['count'], " left" if haveTags else "")}
+        for t in allTags(params.user,
+                         withTags=haveTags)]}
     
 @bottle.route('/<user>/')
 def userSlash(user):
@@ -126,35 +132,32 @@
 
 @bottle.route('/<user>', method='POST')
 def userAddLink(user):
-    p = bottle.request.params
     if getUser()[0] != user:
         raise ValueError("not logged in as %s" % user)
-    links.insertOrUpdate(dict(
-        user=user,
-        description=p.description,
-        extended=p.extended,
-        href=p.href,
-        #private=p.private, == checked,
-        #shared ??
-        tag=p.tag,
-        t=datetime.datetime.now(tzlocal()),
-        ))
+    print repr(bottle.request.params.__dict__)
+    doc = links.fromPostdata(bottle.request.params,
+                             user,
+                             datetime.datetime.now(tzlocal()))
+    links.insertOrUpdate(doc)
+
+    print "notify about sharing to", repr(doc['shareWith'])
         
     bottle.redirect(siteRoot + '/' + user)
     
 @bottle.route('/<user>/<tags>')
 def userLinks(user, tags, toRoot=".."):
-    tags = tags.split('+')
+    tags = filter(None, tags.split('+'))
     data = recentTags(user, tags)
     data['loginBar'] = getLoginBar()
     data['desc'] = ("%s's recent links" % user) + (" tagged %s"  % (tags,) if tags else "")
     data['toRoot'] = toRoot
     data['allTags'] = allTags(user)
+    data['user'] = user
 
     data['pageTags'] = [{"word":t} for t in tags]
     data['stats']['template'] = 'TEMPLATETIME'
     return renderWithTime('links.jade', data)
-
+    
 @bottle.route('/')
 def root():
     data = {
--- a/static/add.js	Wed Mar 06 23:23:18 2013 -0800
+++ b/static/add.js	Fri Mar 15 00:29:53 2013 -0700
@@ -1,23 +1,27 @@
 var model = {
-    href: ko.observable(""),
-    description: ko.observable(""),
-    tag: ko.observable(""),
-    extended: ko.observable(""),
+    linkRecord: {
+        href: ko.observable(""),
+        description: ko.observable(""),
+        tag: ko.observable(""),
+        extended: ko.observable(""),
+        private: ko.observable(false),
+        shareWith: ko.observableArray([]), // foaf uris
+    },
     submitLabel: ko.observable("Add"),
 };
 
 ko.computed(function() {
-    if (model.href() == "") {
+    if (model.linkRecord.href() == "") {
         return;
     }
 
-    $.getJSON("addLink/proposedUri", {uri: model.href()}, function (data) {
+    $.getJSON("addLink/proposedUri", {uri: model.linkRecord.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.linkRecord.description(data.description);
+        model.linkRecord.tag(data.tag);
+        model.linkRecord.extended(data.extended);
+        model.linkRecord.shareWith(data.shareWith);
         model.submitLabel(data.existed ? "Update existing" : "Add");
         
     });
@@ -25,3 +29,43 @@
 });
 
 ko.applyBindings(model);
+
+$("#shareWith").select2({
+    tokenSeparators: [",", " "],
+    ajax: {
+        url: "/foaf/findPerson",
+        data: function (term, page) {
+            return {q: term};
+        },
+        results: function (data, page) {
+            var ret = {results: data.people.map(
+                function (row) {
+                    return {id: row.uri, text: row.label + " ("+row.uri+")"}
+                }),
+                       more: false,
+                       context: {}
+                      };
+            //ret.results.push({id: "new1", text: this.context});
+            return ret;
+        }
+    },
+    tags: [],
+});
+$("#shareWith").on('change', function (e) { setModelFromShares(e.val); });
+
+var setSharesFromModel = ko.computed(
+    function () {
+        var uris = ko.utils.arrayGetDistinctValues(model.linkRecord.shareWith());
+        console.log("from model", uris)
+        $("#shareWith").select2("data", uris.map(
+            function (uri) {
+                return {id: uri, text: "("+uri+")"};
+            }));
+    });
+
+function setModelFromShares(n) {
+    console.log("from val", $("#shareWith").select2("val"), "new", n)
+    model.linkRecord.shareWith($("#shareWith").select2("val"));
+}
+
+setSharesFromModel();
--- a/static/gui.js	Wed Mar 06 23:23:18 2013 -0800
+++ b/static/gui.js	Fri Mar 15 00:29:53 2013 -0700
@@ -1,83 +1,2 @@
-
-$("#filterTag").focus();
-
-var model = {
-    filterTags: ko.observableArray(currentFilter())
-};
-
-function currentFilter() {
-    var p = window.location.pathname;
-    var comps = p.split("/");
-    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);
-    }
-    setPageTags(selected);
-}
 
-function setPageTags(tags) {
-    
-    var newPath = window.location.pathname;
-    if (toRoot == ".") {
-        newPath += "/";
-    } else {
-        newPath = newPath.replace(
-                /(.*\/)[^\/]*$/, "$1")
-    }
-    console.log("user root", newPath);
-    if (tags.length) {
-        newPath += tags.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[comps.length-1] || "").split("+");
-    if (selected.length == 0) {
-        return;
-    }
-    toggleTag(selected[selected.length-1]);
-}
-
-$("a.tag").click(function () {
-    var tag = $(this).text();
-    toggleTag(tag);
-    return false;
-});
-
-$("#filterTag").change(function () {
-    var tags = $(this).val();
-    setPageTags(tags);
-    return false;
-});
-
-$("#filterTag").keydown(function (ev) {
-    if ($(this).val() == "" && ev.which == 8) {
-        backspaceLastTag();
-    }
-});
-
-$("#filterTag").chosen({
-
-    });
-
-ko.applyBindings(model);
-$("#filterTag").trigger("liszt:updated");
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/links.js	Fri Mar 15 00:29:53 2013 -0700
@@ -0,0 +1,98 @@
+
+$("#filterTag").focus();
+
+var model = {
+    filterTags: ko.observableArray(currentFilter())
+};
+
+function currentFilter() {
+    var p = window.location.pathname;
+    var comps = p.split("/");
+    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);
+    }
+    setPageTags(selected);
+}
+
+function setPageTags(tags) {
+    
+    var newPath = window.location.pathname;
+    if (toRoot == ".") {
+        newPath += "/";
+    } else {
+        newPath = newPath.replace(
+                /(.*\/)[^\/]*$/, "$1")
+    }
+    console.log("user root", newPath);
+    if (tags.length) {
+        newPath += tags.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[comps.length-1] || "").split("+");
+    if (selected.length == 0) {
+        return;
+    }
+    toggleTag(selected[selected.length-1]);
+}
+
+$("a.tag").click(function () {
+    var tag = $(this).text();
+    toggleTag(tag);
+    return false;
+});
+
+$("#filterTag").change(function () {
+    var tags = $(this).val().split(",");
+    setPageTags(tags);
+    return false;
+});
+
+var filterCompleteWords = "";
+$("#filterTag").select2({
+    allowClear: true,
+    multiple: true,
+    tokenSeparators: [' ', ','],
+    query: function (opts) {
+        $.ajax({
+            url: toRoot + "/tags",
+            data: {user: user, have: opts.element.val()},
+            success: function (data) {
+                opts.callback({results: data.tags});
+            }
+        });
+    },
+    change: function (ev) {
+        console.log("ch", ev.val);
+    },
+    initSelection: function (element, callback) {
+           var data = [];
+        $(element.val().split(",")).each(function () {
+            data.push({id: this, text: this});
+        });
+        callback(data);
+    }
+});
+$("#filterTag").select2("val", model.filterTags());
+
+ko.applyBindings(model);
--- a/static/style.css	Wed Mar 06 23:23:18 2013 -0800
+++ b/static/style.css	Fri Mar 15 00:29:53 2013 -0700
@@ -54,7 +54,21 @@
     margin-left: 24px;
     color: #5E6A6F;
 }
-input {
+.shareWith {
+    margin-left: 24px;
+    color: #5E6A6F;
+}
+.person {
+display: inline-block;
+padding: 2px;
+margin: 0 2px;
+border: 1px solid gray;
+border-radius: 3px;
+    }
+input[type=text] {
     width: 500px;
     padding: 4px;
+}
+#filterTag {
+    width: 300px;
 }
\ No newline at end of file
--- a/template/add.jade.mustache	Wed Mar 06 23:23:18 2013 -0800
+++ b/template/add.jade.mustache	Fri Mar 15 00:29:53 2013 -0700
@@ -6,43 +6,10 @@
   body
     | {{{loginBar}}}
   
-    h2 add link
-
-    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...';
-    s.style.position='fixed';
-    s.style.zIndex='9999';
-    s.style.border='2px solid black';
-    
-    s.style.top='40px';
-    s.style.right='40px';
-    s.style.bottom='40px';
-    s.style.left='40px';
-    s.style.background='white';
-    document.body.appendChild(s);
-
-    s=document.createElement('script');
-    s.setAttribute('type','text/javascript');
-    s.setAttribute('src','{{absRoot}}/addOverlay'+
-        '?url='+encodeURIComponent(window.location.href)+
-        '&title='+encodeURIComponent(document.title)+
-        '&selected='+encodeURIComponent(''+(
-            window.getSelection ? window.getSelection(): document.getSelection? document.getSelection(): document.selection.createRange().text))
-        );
-    document.body.appendChild(s);
-})();") addlink
+    h2 add link  
 
     form(method='post', action='{{toRoot}}/{{user}}')
+      // ko with: linkRecord
       div URI:
         input(type='text', name='href', data-bind='value: href')
       div Title:
@@ -52,20 +19,27 @@
 
       div Comment:
         textarea(name='extended', data-bind='value: extended')
-      div Private: ...
-      div Shared: ..?
-        | type name to autocomplete from bigfoaf service
-        |    bigfoaf-> people ? q= name_or_email_substr
-        |      -> json list of uri, fullname, email, img
-        | accumulate foaf uris
+      div
+        | Private (no effect, yet):
+        input(type='checkbox', data-bind='value: private')
+      div Share with (this does not work yet):
+        input#shareWith(type='hidden', name='shareWith', style="width: 600px")
+
+      pre          
         | upon submit, send msgs to c3po who has to buffer them against resends.
         | bigfoaf display should talk to c3po to learn msgs sent from and to this person.
-        | 
+      // /ko
       div
         button(type='submit', data-bind="text: submitLabel")
-          Add
+          | Add
+
+    hr
 
+    {{> bookmarklets.jade}}
+          
+    script
+      var toRoot = "{{toRoot}}", user = "{{user}}";
     {{> tail.jade}}
     script(src="{{toRoot}}/static/add.js")
     script
-      model.href({{{fillHrefJson}}})
+      model.linkRecord.href({{{fillHrefJson}}})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/template/bookmarklets.jade.mustache	Fri Mar 15 00:29:53 2013 -0700
@@ -0,0 +1,33 @@
+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...';
+s.style.position='fixed';
+s.style.zIndex='9999';
+s.style.border='2px solid black';
+
+s.style.top='40px';
+s.style.right='40px';
+s.style.bottom='40px';
+s.style.left='40px';
+s.style.background='white';
+document.body.appendChild(s);
+
+s=document.createElement('script');
+s.setAttribute('type','text/javascript');
+s.setAttribute('src','{{absRoot}}/addOverlay'+
+    '?url='+encodeURIComponent(window.location.href)+
+    '&title='+encodeURIComponent(document.title)+
+    '&selected='+encodeURIComponent(''+(
+        window.getSelection ? window.getSelection(): document.getSelection? document.getSelection(): document.selection.createRange().text))
+    );
+document.body.appendChild(s);
+})();") addlink
--- a/template/head.jade.mustache	Wed Mar 06 23:23:18 2013 -0800
+++ b/template/head.jade.mustache	Fri Mar 15 00:29:53 2013 -0700
@@ -1,3 +1,3 @@
 meta(name="viewport", content="width=650")
-link(rel="stylesheet", href="{{toRoot}}/static/lib/chosen.css")
+link(rel="stylesheet", href="{{toRoot}}/static/lib/select2-3.3.1/select2.css")
 link(rel="stylesheet", href="{{toRoot}}/static/style.css")
\ No newline at end of file
--- a/template/index.jade.mustache	Wed Mar 06 23:23:18 2013 -0800
+++ b/template/index.jade.mustache	Fri Mar 15 00:29:53 2013 -0700
@@ -24,7 +24,6 @@
       li
         a(href='https://code.google.com/p/gnizr/') gnizer
         
-    
     p {{stats}}
     
     {{> tail.jade}}
--- a/template/links.jade.mustache	Wed Mar 06 23:23:18 2013 -0800
+++ b/template/links.jade.mustache	Fri Mar 15 00:29:53 2013 -0700
@@ -11,10 +11,7 @@
       a(href="{{toRoot}}/addLink") Add link
     
     | Filter to: 
-    select#filterTag(multiple='multiple', style='width: 300px', data-bind="selectedOptions: filterTags")
-      {{#allTags}}
-      option(value="{{label}}") {{label}} ({{count}})
-      {{/allTags}}
+    input#filterTag(type='hidden', style="width: 300px")
     
     {{#links}}
     .link
@@ -35,6 +32,12 @@
         a(href="{{href}}") {{href}}
       .modified
         | Modified: {{t}}
+      .shareWith
+        | Share with:
+        {{#shareWith}}
+        span.person
+          | {{label}}
+        {{/shareWith}}
       .edit
         a(href="{{toRoot}}/{{editLink}}") Edit
         
@@ -42,5 +45,6 @@
 
     p {{stats}}
     script
-      var toRoot = "{{toRoot}}";
+      var toRoot = "{{toRoot}}", user = "{{user}}";
     {{> tail.jade}}
+    script(src="{{toRoot}}/static/links.js")
--- a/template/tail.jade.mustache	Wed Mar 06 23:23:18 2013 -0800
+++ b/template/tail.jade.mustache	Fri Mar 15 00:29:53 2013 -0700
@@ -1,4 +1,4 @@
-script(src='//ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js')
-script(src='{{toRoot}}/static/lib/chosen-0.9.11.jquery.min.js')
+script(src='{{toRoot}}/static/lib/jquery-1.9.1.min.js')
+script(src='{{toRoot}}/static/lib/select2-3.3.1/select2.min.js')
 script(src="{{toRoot}}/static/lib/knockout-2.2.0.debug.js")
 script(src="{{toRoot}}/static/gui.js")