# HG changeset patch # User drewp@bigasterisk.com # Date 1363332593 25200 # Node ID e054949143e9fc3addeacf55d5f1bc6c99602503 # Parent 7df920c18c8346084920f56b06523aee22c7fdae reworking addlink and shareWith support Ignore-this: b1665b776f3964f7fde219acadc51f32 diff -r 7df920c18c83 -r e054949143e9 link.py --- 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, + ) diff -r 7df920c18c83 -r e054949143e9 lookup.py --- 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('//') def userSlash(user): @@ -126,35 +132,32 @@ @bottle.route('/', 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('//') 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 = { diff -r 7df920c18c83 -r e054949143e9 static/add.js --- 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(); diff -r 7df920c18c83 -r e054949143e9 static/gui.js --- 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"); - diff -r 7df920c18c83 -r e054949143e9 static/links.js --- /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); diff -r 7df920c18c83 -r e054949143e9 static/style.css --- 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 diff -r 7df920c18c83 -r e054949143e9 template/add.jade.mustache --- 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}}}) diff -r 7df920c18c83 -r e054949143e9 template/bookmarklets.jade.mustache --- /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 diff -r 7df920c18c83 -r e054949143e9 template/head.jade.mustache --- 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 diff -r 7df920c18c83 -r e054949143e9 template/index.jade.mustache --- 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}} diff -r 7df920c18c83 -r e054949143e9 template/links.jade.mustache --- 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") diff -r 7df920c18c83 -r e054949143e9 template/tail.jade.mustache --- 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")