Skip to content

Instantly share code, notes, and snippets.

@tilgovi
Created September 23, 2015 17:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tilgovi/59e74d9c5dd0b3c4b8a7 to your computer and use it in GitHub Desktop.
Save tilgovi/59e74d9c5dd0b3c4b8a7 to your computer and use it in GitHub Desktop.
SQLAlchemy-style mapper class beginnings in CoffeeScript, for Hypothesis
commit 962fe64b4dd5b0a4b02fda6f08a7c81d0e40d961
Merge: 367c72c 595968e
Author: Randall Leeds <tilgovi@hypothes.is>
Date: Mon Feb 9 14:47:14 2015 -0800
WIP on master: 367c72c Remove selenium test requirement
diff --cc h/static/scripts/annotation-mapper-service.coffee
index 62d2c08,62d2c08..d95fed1
--- a/h/static/scripts/annotation-mapper-service.coffee
+++ b/h/static/scripts/annotation-mapper-service.coffee
@@@ -1,30 -1,30 +1,89 @@@
# Wraps the annotation store to trigger events for the CRUD actions
class AnnotationMapperService
-- this.$inject = ['$rootScope', 'threading', 'store']
-- constructor: ($rootScope, threading, store) ->
-- this.setupAnnotation = (ann) -> ann
++ this.$inject = ['$q', '$rootScope', 'threading', 'store']
++ constructor: ( $q, $rootScope, threading, store) ->
++ this.new = []
++ this.deleted = []
++ this.dirty = []
-- this.loadAnnotations = (annotations) ->
-- annotations = for annotation in annotations
-- container = threading.idTable[annotation.id]
-- if container?.message
-- angular.copy(annotation, container.message)
-- $rootScope.$emit('annotationUpdated', container.message)
++ this.add = (annotation) ->
++ if annotation.id
++ @dirty.push(annotation)
++ else
++ @new.push(annotation)
++
++ this.delete = (annotation) ->
++ @deleted.push annotation
++
++ this.expunge = (annotation) ->
++ $rootScope.$emit('annotationDeleted', annotation)
++
++ this.flush = (annotations) ->
++ if annotations
++ shouldFlush = (s) -> s in annotations
++ else
++ shouldFlush = -> true
++
++ promises = []
++
++ @new = for pending in @new
++ if shouldFlush(pending)
++ promise = pending.$create().then (result) ->
++ $rootScope.$emit('annotationCreated', result)
++ promises.push(promise)
continue
else
-- annotation
++ pending
-- annotations = (new store.AnnotationResource(a) for a in annotations)
-- $rootScope.$emit('annotationsLoaded', annotations)
++ @deleted = for pending in @deleted
++ if shouldFlush(pending)
++ promise = pending.$delete(id: pending.id).then (result) ->
++ $rootScope.$emit('annotationDeleted', result)
++ promises.push(promise)
++ continue
++ else
++ pending
++
++ @dirty = for pending in @dirty
++ if shouldFlush(pending)
++ promise = pending.$update(id: pending.id).then (result) ->
++ $rootScope.$emit('annotationUpdated', result)
++ promises.push(promise)
++ continue
++ else
++ pending
++
++ $q.all(promises)
++
++ this.merge = (annotation, load=true) ->
++ if annotation.id
++ container = threading.idTable[annotation.id]
++ if container?.message
++ instance = angular.copy(annotation, container.message)
++ $rootScope.$emit('annotationUpdated', instance)
++ else if load
++ params = id: annotation.id
++ instance = store.AnnotationResource.read params, ->
++ instance = angular.copy(annotation, instance)
++ $rootScope.$emit('annotationsLoaded', [instance])
++ else
++ instance = new store.AnnotationResource(annotation)
++ else
++ instance = new store.AnnotationResource(annotation)
++ $rootScope.$emit('beforeAnnotationCreated', instance)
++ instance
-- this.createAnnotation = (annotation) ->
-- annotation = new store.AnnotationResource(annotation)
-- $rootScope.$emit('beforeAnnotationCreated', annotation)
-- annotation
++ this.query = (query) ->
++ result = store.SearchResource.get(query)
++ result.$promise.then ({rows}) =>
++ rows = for annotation in rows
++ annotation = this.merge(annotation, false)
++ if annotation.$promise
++ continue
++ else
++ annotation
++ $rootScope.$emit('annotationsLoaded', rows)
++ rows
-- this.deleteAnnotation = (annotation) ->
-- annotation.$delete(id: annotation.id).then ->
-- $rootScope.$emit('annotationDeleted', annotation)
-- annotation
angular.module('h').service('annotationMapper', AnnotationMapperService)
diff --cc h/static/scripts/annotation-sync.coffee
index 3dde1ec,3dde1ec..97fa02a
--- a/h/static/scripts/annotation-sync.coffee
+++ b/h/static/scripts/annotation-sync.coffee
@@@ -13,7 -13,7 +13,7 @@@ class AnnotationSyn
# to reconcile any differences. The default behavior is to merge all
# keys of the remote object into the local copy
merge: (local, remote) ->
-- for k, v of remote
++ for own k, v of remote
local[k] = v
local
diff --cc h/static/scripts/auth-service.coffee
index d9c6896,d9c6896..c81ddf0
--- a/h/static/scripts/auth-service.coffee
+++ b/h/static/scripts/auth-service.coffee
@@@ -18,21 -18,21 +18,6 @@@ class Aut
_checkingToken = false
@user = undefined
-- # TODO: Remove this once Auth has been migrated.
-- $rootScope.$on 'beforeAnnotationCreated', (event, annotation) =>
-- annotation.user = @user
-- annotation.permissions = {}
-- annotator.publish('beforeAnnotationCreated', annotation)
--
-- $rootScope.$on 'annotationCreated', (event, annotation) =>
-- annotator.publish('annotationCreated', annotation)
--
-- $rootScope.$on 'annotationUpdated', (event, annotation) =>
-- annotator.publish('annotationUpdated', annotation)
--
-- $rootScope.$on 'beforeAnnotationUpdated', (event, annotation) =>
-- annotator.publish('beforeAnnotationUpdated', annotation)
--
# Fired when the identity-service successfully requests authentication.
# Sets up the Annotator.Auth plugin instance and the auth.user property.
# It sets a flag between that time period to indicate that the token is
diff --cc h/static/scripts/controllers.coffee
index 44220d7,44220d7..d7a775c
--- a/h/static/scripts/controllers.coffee
+++ b/h/static/scripts/controllers.coffee
@@@ -1,38 -1,38 +1,12 @@@
--# Watch the UI state and update scope properties.
--class AnnotationUIController
-- this.$inject = ['$rootScope', '$scope', 'annotationUI']
-- constructor: ( $rootScope, $scope, annotationUI ) ->
-- $rootScope.$watch (-> annotationUI.selectedAnnotationMap), (map={}) ->
-- count = Object.keys(map).length
-- $scope.selectedAnnotationsCount = count
--
-- if count
-- $scope.selectedAnnotations = map
-- else
-- $scope.selectedAnnotations = null
--
-- $rootScope.$watch (-> annotationUI.focusedAnnotationMap), (map={}) ->
-- $scope.focusedAnnotations = map
--
-- $rootScope.$on 'annotationDeleted', (event, annotation) ->
-- annotationUI.removeSelectedAnnotation(annotation)
--
--
class AppController
this.$inject = [
-- '$controller', '$document', '$location', '$route', '$scope', '$window',
-- 'auth', 'drafts', 'identity',
-- 'permissions', 'streamer', 'streamfilter', 'annotationUI',
-- 'annotationMapper', 'threading'
++ '$document', '$location', '$route', '$scope', '$window',
++ 'auth', 'drafts', 'identity', 'streamer', 'streamfilter', 'annotationMapper'
]
constructor: (
-- $controller, $document, $location, $route, $scope, $window,
-- auth, drafts, identity,
-- permissions, streamer, streamfilter, annotationUI,
-- annotationMapper, threading
++ $document, $location, $route, $scope, $window,
++ auth, drafts, identity, streamer, streamfilter, annotationMapper
) ->
-- $controller(AnnotationUIController, {$scope})
--
$scope.auth = auth
isFirstRun = $location.search().hasOwnProperty('firstrun')
@@@ -45,10 -45,10 +19,14 @@@
return unless data?.length
switch action
when 'create', 'update', 'past'
-- annotationMapper.loadAnnotations data
++ load = for annotation in data
++ annotation = annotationMapper.merge(annotation, false)
++ if annotation.$promise then continue else annotation
++ $scope.$emit('annotationsLoaded', load)
when 'delete'
for annotation in data
-- $scope.$emit('annotationDeleted', annotation)
++ annotation = annotationMapper.merge(annotation, false)
++ annotationMapper.expunge(annotation)
streamer.onmessage = (data) ->
return if !data or data.type != 'annotation-notification'
@@@ -60,18 -60,18 +38,6 @@@
oncancel = ->
$scope.dialog.visible = false
-- cleanupAnnotations = ->
-- # Clean up any annotations that need to be unloaded.
-- for id, container of $scope.threading.idTable when container.message
-- # Remove annotations not belonging to this user when highlighting.
-- if annotationUI.tool is 'highlight' and annotation.user != auth.user
-- $scope.$emit('annotationDeleted', container.message)
-- drafts.remove annotation
-- # Remove annotations the user is not authorized to view.
-- else if not permissions.permits 'read', container.message, auth.user
-- $scope.$emit('annotationDeleted', container.message)
-- drafts.remove container.message
--
$scope.$watch 'sort.name', (name) ->
return unless name
predicate = switch name
@@@ -88,17 -88,17 +54,10 @@@
else
$scope.dialog.visible = false
-- # Update any edits in progress.
-- for draft in drafts.all()
-- $scope.$emit('beforeAnnotationCreated', draft)
--
# Reopen the streamer.
streamer.close()
streamer.open($window.WebSocket, streamerUrl)
-- # Clean up annotations that should be removed
-- cleanupAnnotations()
--
# Reload the view.
$route.reload()
@@@ -116,7 -116,7 +75,6 @@@
$scope.clearSelection = ->
$scope.search.query = ''
-- annotationUI.clearSelectedAnnotations()
$scope.dialog = visible: false
@@@ -129,21 -129,21 +87,18 @@@
update: (query) ->
unless angular.equals $location.search()['q'], query
$location.search('q', query or null)
-- annotationUI.clearSelectedAnnotations()
$scope.sort = name: 'Location'
-- $scope.threading = threading
-- $scope.threadRoot = $scope.threading?.root
class AnnotationViewerController
this.$inject = [
'$location', '$routeParams', '$scope',
-- 'streamer', 'store', 'streamfilter', 'annotationMapper'
++ 'streamer', 'streamfilter', 'threading', 'annotationMapper'
]
constructor: (
$location, $routeParams, $scope,
-- streamer, store, streamfilter, annotationMapper
++ streamer, streamfilter, threading, annotationMapper
) ->
# Tells the view that these annotations are standalone
$scope.isEmbedded = false
@@@ -159,11 -159,11 +114,9 @@@
$location.path('/stream').search('q', query)
id = $routeParams.id
-- store.SearchResource.get _id: id, ({rows}) ->
-- annotationMapper.loadAnnotations(rows)
-- $scope.threadRoot = children: [$scope.threading.getContainer(id)]
-- store.SearchResource.get references: id, ({rows}) ->
-- annotationMapper.loadAnnotations(rows)
++ annotationMapper.query(_id: id).then ->
++ $scope.threadRoot = children: [threading.getContainer(id)]
++ annotationMapper.query(references: id)
streamfilter
.setMatchPolicyIncludeAny()
@@@ -175,15 -175,15 +128,16 @@@
class ViewerController
this.$inject = [
'$scope', '$route', 'annotationUI', 'crossframe', 'annotationMapper',
-- 'auth', 'flash', 'streamer', 'streamfilter', 'store'
++ 'auth', 'permissions', 'streamer', 'streamfilter', 'store', 'threading'
]
constructor: (
$scope, $route, annotationUI, crossframe, annotationMapper,
-- auth, flash, streamer, streamfilter, store
++ auth, permissions, streamer, streamfilter, store, threading
) ->
# Tells the view that these annotations are embedded into the owner doc
$scope.isEmbedded = true
$scope.isStream = true
++ $scope.threadRoot = threading.root
loaded = []
@@@ -196,8 -196,8 +150,7 @@@
for p in crossframe.providers
for e in p.entities when e not in loaded
loaded.push e
-- r = store.SearchResource.get angular.extend(uri: e, query), (results) ->
-- annotationMapper.loadAnnotations(results.rows)
++ annotationMapper.query(angular.extend(uri: e, query))
streamfilter.resetFilter().addClause('/uri', 'one_of', loaded)
@@@ -206,10 -206,10 +159,38 @@@
streamer.send({filter: streamfilter.getFilter()})
++ # Clean up any annotations that need to be unloaded.
++ for id, container of threading.idTable when container.message
++ annotation = container.message
++ # Remove annotations not belonging to this user when highlighting.
++ if annotationUI.tool is 'highlight' and annotation.user != auth.user
++ annotationMapper.expunge annotation
++ # Remove annotations the user is not authorized to view.
++ else if not permissions.permits 'read', annotation, auth.user
++ annotationMapper.expunge annotation
++
++ $scope.$on 'annotationDeleted', (event, annotation) ->
++ annotationUI.removeSelectedAnnotation(annotation)
++
++ $scope.$on '$routeUpdate', ->
++ annotationUI.clearSelectedAnnotations()
++
$scope.$watch (-> annotationUI.tool), (newVal, oldVal) ->
return if newVal is oldVal
$route.reload()
++ $scope.$watch (-> annotationUI.selectedAnnotationMap), (map={}) ->
++ count = Object.keys(map).length
++ $scope.selectedAnnotationsCount = count
++
++ if count
++ $scope.selectedAnnotations = map
++ else
++ $scope.selectedAnnotations = null
++
++ $scope.$watch (-> annotationUI.focusedAnnotationMap), (map={}) ->
++ $scope.focusedAnnotations = map
++
$scope.$watchCollection (-> crossframe.providers), loadAnnotations
$scope.focus = (annotation) ->
@@@ -240,4 -240,4 +221,3 @@@ angular.module('h'
.controller('AppController', AppController)
.controller('ViewerController', ViewerController)
.controller('AnnotationViewerController', AnnotationViewerController)
--.controller('AnnotationUIController', AnnotationUIController)
diff --cc h/static/scripts/cross-frame-service.coffee
index 27421bb,27421bb..e4ea711
--- a/h/static/scripts/cross-frame-service.coffee
+++ b/h/static/scripts/cross-frame-service.coffee
@@@ -31,12 -31,12 +31,11 @@@ class CrossFrameServic
options =
formatter: (annotation) ->
formatted = {}
-- for k, v of annotation when k in whitelist
++ for own k, v of annotation when k in whitelist
formatted[k] = v
formatted
parser: (annotation) ->
-- parsed = new store.AnnotationResource()
-- for k, v of annotation when k in whitelist
++ for own k, v of annotation when k in whitelist
parsed[k] = v
parsed
emit: (args...) ->
diff --cc h/static/scripts/directives/annotation.coffee
index 91e5dd2,91e5dd2..a34a20a
--- a/h/static/scripts/directives/annotation.coffee
+++ b/h/static/scripts/directives/annotation.coffee
@@@ -94,7 -94,7 +94,11 @@@ AnnotationController =
###
this.delete = ->
if confirm "Are you sure you want to delete this annotation?"
-- annotationMapper.deleteAnnotation model
++ annotationMapper.delete(model)
++ annotationMapper.flush([model])
++ .catch ->
++ flash 'error',
++ 'Something went wrong while deleting the annotation.'
###*
# @ngdoc method
@@@ -115,7 -115,7 +119,7 @@@
this.revert = ->
drafts.remove model
if @action is 'create'
-- $rootScope.$emit('annotationDeleted', model)
++ annotationMapper.expunge(model)
else
this.render()
@action = 'view'
@@@ -148,16 -148,16 +152,14 @@@
angular.extend model, @annotation,
tags: (tag.text for tag in @annotation.tags)
-- switch @action
-- when 'create'
-- model.$create().then ->
-- $rootScope.$emit('annotationCreated', model)
-- when 'delete', 'edit'
-- model.$update(id: model.id).then ->
-- $rootScope.$emit('annotationUpdated', model)
--
-- @editing = false
-- @action = 'view'
++ annotationMapper.add(model)
++ annotationMapper.flush([model])
++ .then (model) =>
++ @editing = false
++ @action = 'view'
++ .catch ->
++ flash 'error',
++ 'Something went wrong while saving the annotation.'
###*
# @ngdoc method
@@@ -174,7 -174,7 +176,7 @@@
# Construct the reply.
references = [references..., id]
-- reply = annotationMapper.createAnnotation({references, uri})
++ reply = annotationMapper.merge({references, uri})
if auth.user?
if permissions.isPublic model.permissions
@@@ -253,6 -253,6 +255,23 @@@
updateTimestamp = angular.noop
drafts.remove model
++ # Watch the user.
++ $scope.$watch (-> auth.user), (user, old) =>
++ return if model.id
++
++ model.permissions = {}
++ model.user = user
++
++ # Save highlights once logged in.
++ if highlight and this.isHighlight()
++ highlight = false # skip this on future updates
++ if user
++ model.permissions = permissions.private()
++ annotationMapper.add(model)
++ annotationMapper.flush([model])
++ else
++ drafts.add model, => this.revert()
++
# Watch the model.
# XXX: TODO: don't clobber the view when collaborating
$scope.$watch (-> model), (model, old) =>
@@@ -263,18 -263,18 +282,7 @@@
# Propagate an update event up the thread (to pulse changing threads),
# but only if this is someone else's annotation.
if model.user != auth.user
-- $scope.$emit('annotationUpdate')
--
-- # Save highlights once logged in.
-- if highlight and this.isHighlight()
-- if model.user and not model.id
-- highlight = false # skip this on future updates
-- model.permissions = permissions.private()
-- model.$create().then ->
-- $rootScope.$emit('annotationCreated', model)
-- highlight = false # skip this on future updates
-- else
-- drafts.add model, => this.revert()
++ $scope.$emit('annotationUpdate', model)
updateTimestamp(model is old) # repeat on first run
this.render()
diff --cc h/static/scripts/streamsearch.coffee
index b47cd47,b47cd47..9c123fd
--- a/h/static/scripts/streamsearch.coffee
+++ b/h/static/scripts/streamsearch.coffee
@@@ -1,13 -1,13 +1,13 @@@
class StreamSearchController
this.inject = [
'$scope', '$rootScope', '$routeParams',
-- 'auth', 'queryparser', 'searchfilter', 'store',
-- 'streamer', 'streamfilter', 'annotationMapper'
++ 'flash', 'queryparser', 'searchfilter',
++ 'streamer', 'streamfilter', 'threading', 'annotationMapper'
]
constructor: (
$scope, $rootScope, $routeParams
-- auth, queryparser, searchfilter, store,
-- streamer, streamfilter, annotationMapper
++ flash, queryparser, searchfilter,
++ streamer, streamfilter, threading, annotationMapper
) ->
# Initialize the base filter
streamfilter
@@@ -23,11 -23,11 +23,13 @@@
# Perform the search
searchParams = searchfilter.toObject $scope.search.query
query = angular.extend limit: 10, searchParams
-- store.SearchResource.get query, ({rows}) ->
-- annotationMapper.loadAnnotations(rows)
++ annotationMapper.query(query)
++ .catch ->
++ flash 'error', 'Something went wrong while fetching annotations.'
$scope.isEmbedded = false
$scope.isStream = true
++ $scope.threadRoot = threading.root
$scope.sort.name = 'Newest'
diff --cc h/static/scripts/threading-service.coffee
index 0002fb8,0002fb8..64704d5
--- a/h/static/scripts/threading-service.coffee
+++ b/h/static/scripts/threading-service.coffee
@@@ -27,8 -27,8 +27,6 @@@ class ThreadingServic
thread = (this.getContainer message.id)
thread.message = message
else
-- # XXX: relies on outside code to update the idTable if the message
-- # later acquires an id.
thread = mail.messageContainer(message)
prev = @root
diff --cc tests/js/annotation-ui-sync-test.coffee
index a585538,a585538..380a8a5
--- a/tests/js/annotation-ui-sync-test.coffee
+++ b/tests/js/annotation-ui-sync-test.coffee
@@@ -96,6 -96,6 +96,19 @@@ describe 'AnnotationUISync', -
publish({method: 'open'})
assert.called($digest)
++ describe 'on "back" event', ->
++ it 'sends the "hideFrame" message to the host only', ->
++ createAnnotationUISync()
++ publish({method: 'back'})
++ assert.calledWith(fakeBridge.links[0].channel.notify, method: 'hideFrame')
++ assert.notCalled(fakeBridge.links[1].channel.notify)
++ assert.notCalled(fakeBridge.links[2].channel.notify)
++
++ it 'triggers a digest', ->
++ createAnnotationUISync()
++ publish({method: 'back'})
++ assert.called($digest)
++
describe 'on "showEditor" event', ->
it 'sends the "showFrame" message to the host only', ->
createAnnotationUISync()
diff --cc tests/js/directives/annotation-test.coffee
index eecb0cc,eecb0cc..f235cc1
--- a/tests/js/directives/annotation-test.coffee
+++ b/tests/js/directives/annotation-test.coffee
@@@ -115,14 -115,14 +115,6 @@@ describe 'h.directives.annotation', -
controller.reply()
assert.notInclude(reply.permissions.read, 'group:__world__')
-- it 'fills the other permissions too', ->
-- reply = {}
-- fakeAnnotationMapper.createAnnotation.returns(reply)
-- controller.reply()
-- assert.equal(reply.permissions.update[0], 'acct:bill@localhost')
-- assert.equal(reply.permissions.delete[0], 'acct:bill@localhost')
-- assert.equal(reply.permissions.admin[0], 'acct:bill@localhost')
--
describe '#render', ->
controller = null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment