Tabs to spaces

Tabs to spaces

--- src/coffee/obscura.coffee
+++ src/coffee/obscura.coffee
@@ -1,213 +1,213 @@
 #set up global object
 root = exports ? this
-
+
 obscura = (img,target=null) ->

-	###
-	internal variables
-	###
-	fileRegExp = /\.((jp(e)?g)|(png)|(gif))$/i
-	@onLoad = =>
-		return true
-
-	###
-	Set up local variables with image data from source
-	###
-	@setUpImageData = =>
-		@dimensions =
-				w:@image.width
-				h:@image.height
-		@imageDimensions = @dimensions
-		@canvas.width = @canvas.height = if @dimensions.w>@dimensions.h then @dimensions.w*2 else @dimensions.h*2
-
-		#initial load of image
-		@load(0,0,@image.width,@image.height,@image);
-
-	###
-	load image
-	###
-	@load =(x=0,y=0,w=@image.width,h=@image.height,image=@target)=>
-		@context.globalCompositeOperation = "copy"
-		@context.drawImage(image,0,0,@imageDimensions.w,@imageDimensions.h,x,y,w,h)
-		@imageDimensions = {w,h}
-		@render()
-		return @
-
-	###
-	render edited image to target
-	###
-	@render = =>
-		@target.width = @dimensions.w
-		@target.height = @dimensions.h
-		@target.getContext('2d').globalCompositeOperation = "copy"
-		@target.getContext('2d').drawImage(@canvas,0,0)
-		@context.clearRect(0,0,@canvas.width,@canvas.height)
-		return @
-
-	###
-	Convert image to base64 encoded string for saving to server
-	###
-	@save = =>
-		return @target.toDataURL()
-
-	###
-	resizes an image
-	###
-	@resize = (scale,keepProportions=true,crop=false) =>
-		@context.save()
-		#check type of scale and convert it to an object if not already
-		if Object.prototype.toString.call(scale) is '[object Array]'
-			scale =
-				w: scale[0]
-				h:scale[1]
-		else if typeof scale isnt 'object'
-			scale =
-				w:scale
-				h:scale
-
-		#convert percentages to actual values
-		scale.w = if typeof scale.w is 'string' and scale.w.match(/%/) then @dimensions.w * (parseFloat(scale.w)/100) else parseFloat(scale.w)
-		scale.h = if typeof scale.h is 'string' and scale.h.match(/%/) then @dimensions.h * (parseFloat(scale.h)/100) else parseFloat(scale.h)
-
-		currentDimensions = @dimensions
-		@dimensions = scale
-
-		newScale = scale
-		#if keepProportions force stop image distorting
-		if keepProportions
-			if scale.w > scale.h or (scale.w is scale.h and currentDimensions.w > currentDimensions.h)
-				newScale.w = scale.w
-				newScale.h = (scale.w/currentDimensions.w)*currentDimensions.h
-			else if scale.h>scale.w or (scale.h is scale.w and currentDimensions.h > currentDimensions.w)
-				newScale.w = (scale.h/currentDimensions.h)*currentDimensions.w
-				newScale.h = scale.h
-
-			@dimensions = newScale unless crop
-
-		@context.drawImage(@target,0,0,newScale.w,newScale.h)
-		@render()
-		@imageDimensions = @dimensions
-		@context.restore()
-		return @
-
-	###
-	Crops an image
-	###
-	@crop = (x,y,w,h) =>
-		@context.save()
-		size = @dimensions
-		@dimensions = {w,h}
-		@context.drawImage(@target,x,y,w,h,0,0,w,h)
-		@imageDimensions = {w,h}
-		@render()
-		@context.restore()
-		return @
-
-	###
-	Resizes an image to fit completely into a given space
-	###
-	@fit = (w,h) =>
-		@context.save()
-		return @ if w>@canvas.width and h>@canvas.height
-
-		if w<h or @imageDimensions.w > @imageDimensions.h or (w is h and @imageDimensions.h > @imageDimensions.w)
-			h = (w/@imageDimensions.w)*@imageDimensions.h
-		else if h<w or @imageDimensions.h>@imageDimensions.w or (h is w and @imageDimensions.w > @imageDimensions.h)
-			w = (h/@imageDimensions.h)*@imageDimensions.w;
-
-		@dimensions = {w,h}
-		@context.drawImage(@target,0,0,w,h)
-		@imageDimensions = @dimensions
-		@render()
-		@context.restore()
-		return @
-
-	###
-	Rotates an image
-	###
-	@rotate = (angle,center='center') =>
-		@context.restore();
-		@context.save();
-		{w,h}=@dimensions
-		if angle is 90 or angle is 120
-			{cw,ch} = {h,w}
-		else if angle isnt 180 or angle isnt 360
-			cw = w*Math.cos(angle * Math.PI/180) + h*Math.sin(angle * Math.PI/180)
-			ch = h*Math.cos(angle * Math.PI/180) + w*Math.sin(angle * Math.PI/180)
-
-		@context.globalCompositeOperation = "copy";
-
-		if typeof center isnt 'object'
-			if center.match(/(top)/)
-				y = 0
-				y2 = (ch-h)/2
-			else if center.match(/(bottom)/)
-				y = ch
-				y2 = ch - (ch-h)/2
-			else if center.match(/(center)/)
-				y = ch/2
-				y2 = h/2
-
-			if center.match(/(left)/)
-				x = 0
-				x2 = (cw-w)/2
-			else if center.match(/(middle)/)
-				x = cw
-				x2 = cw - (cw-w)/2
-			else if center.match(/(center)/)
-				x = cw/2
-				x2 = w/2
-
-		@dimensions.w = cw
-		@dimensions.h = ch
-		@context.translate(x,y)
-		@context.rotate(angle * Math.PI/180)
-		@context.clearRect(0,0,@canvas.width,@canvas.height)
-		@context.drawImage(@target,0,0,w,h,-x2,-y2,w,h)
-
-		@imageDimensions = @dimensions
-		@context.restore()
-		@render()
-		return @
-
-	###
-	Flips an image
-	###
-	@flip = (direction='horizontal')=>
-		@context.save()
-		if direction is 'horizontal'
-			@context.translate(@dimensions.w, 0);
-			@context.scale(-1,1)
-		else
-			@context.translate(0,@dimensions.h);
-			@context.scale(1,-1)
-
-		@context.drawImage(@target,0,0)
-		@context.restore()
-		@render()
-		return @
-
-	###
-	Reflection
-	###
-	@reflect = (alphaStart=0.5,gap=0,reflectionAmount=0.25,direction = 'vertical')=>
+    ###
+    internal variables
+    ###
+    fileRegExp = /\.((jp(e)?g)|(png)|(gif))$/i
+    @onLoad = =>
+        return true
+
+    ###
+    Set up local variables with image data from source
+    ###
+    @setUpImageData = =>
+        @dimensions =
+                w:@image.width
+                h:@image.height
+        @imageDimensions = @dimensions
+        @canvas.width = @canvas.height = if @dimensions.w>@dimensions.h then @dimensions.w*2 else @dimensions.h*2
+
+        #initial load of image
+        @load(0,0,@image.width,@image.height,@image);
+
+    ###
+    load image
+    ###
+    @load =(x=0,y=0,w=@image.width,h=@image.height,image=@target)=>
+        @context.globalCompositeOperation = "copy"
+        @context.drawImage(image,0,0,@imageDimensions.w,@imageDimensions.h,x,y,w,h)
+        @imageDimensions = {w,h}
+        @render()
+        return @
+
+    ###
+    render edited image to target
+    ###
+    @render = =>
+        @target.width = @dimensions.w
+        @target.height = @dimensions.h
+        @target.getContext('2d').globalCompositeOperation = "copy"
+        @target.getContext('2d').drawImage(@canvas,0,0)
+        @context.clearRect(0,0,@canvas.width,@canvas.height)
+        return @
+
+    ###
+    Convert image to base64 encoded string for saving to server
+    ###
+    @save = =>
+        return @target.toDataURL()
+
+    ###
+    resizes an image
+    ###
+    @resize = (scale,keepProportions=true,crop=false) =>
+        @context.save()
+        #check type of scale and convert it to an object if not already
+        if Object.prototype.toString.call(scale) is '[object Array]'
+            scale =
+                w: scale[0]
+                h:scale[1]
+        else if typeof scale isnt 'object'
+            scale =
+                w:scale
+                h:scale
+
+        #convert percentages to actual values
+        scale.w = if typeof scale.w is 'string' and scale.w.match(/%/) then @dimensions.w * (parseFloat(scale.w)/100) else parseFloat(scale.w)
+        scale.h = if typeof scale.h is 'string' and scale.h.match(/%/) then @dimensions.h * (parseFloat(scale.h)/100) else parseFloat(scale.h)
+
+        currentDimensions = @dimensions
+        @dimensions = scale
+
+        newScale = scale
+        #if keepProportions force stop image distorting
+        if keepProportions
+            if scale.w > scale.h or (scale.w is scale.h and currentDimensions.w > currentDimensions.h)
+                newScale.w = scale.w
+                newScale.h = (scale.w/currentDimensions.w)*currentDimensions.h
+            else if scale.h>scale.w or (scale.h is scale.w and currentDimensions.h > currentDimensions.w)
+                newScale.w = (scale.h/currentDimensions.h)*currentDimensions.w
+                newScale.h = scale.h
+
+            @dimensions = newScale unless crop
+
+        @context.drawImage(@target,0,0,newScale.w,newScale.h)
+        @render()
+        @imageDimensions = @dimensions
+        @context.restore()
+        return @
+
+    ###
+    Crops an image
+    ###
+    @crop = (x,y,w,h) =>
+        @context.save()
+        size = @dimensions
+        @dimensions = {w,h}
+        @context.drawImage(@target,x,y,w,h,0,0,w,h)
+        @imageDimensions = {w,h}
+        @render()
+        @context.restore()
+        return @
+
+    ###
+    Resizes an image to fit completely into a given space
+    ###
+    @fit = (w,h) =>
+        @context.save()
+        return @ if w>@canvas.width and h>@canvas.height
+
+        if w<h or @imageDimensions.w > @imageDimensions.h or (w is h and @imageDimensions.h > @imageDimensions.w)
+            h = (w/@imageDimensions.w)*@imageDimensions.h
+        else if h<w or @imageDimensions.h>@imageDimensions.w or (h is w and @imageDimensions.w > @imageDimensions.h)
+            w = (h/@imageDimensions.h)*@imageDimensions.w;
+
+        @dimensions = {w,h}
+        @context.drawImage(@target,0,0,w,h)
+        @imageDimensions = @dimensions
+        @render()
+        @context.restore()
+        return @
+
+    ###
+    Rotates an image
+    ###
+    @rotate = (angle,center='center') =>
+        @context.restore();
+        @context.save();
+        {w,h}=@dimensions
+        if angle is 90 or angle is 120
+            {cw,ch} = {h,w}
+        else if angle isnt 180 or angle isnt 360
+            cw = w*Math.cos(angle * Math.PI/180) + h*Math.sin(angle * Math.PI/180)
+            ch = h*Math.cos(angle * Math.PI/180) + w*Math.sin(angle * Math.PI/180)
+
+        @context.globalCompositeOperation = "copy";
+
+        if typeof center isnt 'object'
+            if center.match(/(top)/)
+                y = 0
+                y2 = (ch-h)/2
+            else if center.match(/(bottom)/)
+                y = ch
+                y2 = ch - (ch-h)/2
+            else if center.match(/(center)/)
+                y = ch/2
+                y2 = h/2
+
+            if center.match(/(left)/)
+                x = 0
+                x2 = (cw-w)/2
+            else if center.match(/(middle)/)
+                x = cw
+                x2 = cw - (cw-w)/2
+            else if center.match(/(center)/)
+                x = cw/2
+                x2 = w/2
+
+        @dimensions.w = cw
+        @dimensions.h = ch
+        @context.translate(x,y)
+        @context.rotate(angle * Math.PI/180)
+        @context.clearRect(0,0,@canvas.width,@canvas.height)
+        @context.drawImage(@target,0,0,w,h,-x2,-y2,w,h)
+
+        @imageDimensions = @dimensions
+        @context.restore()
+        @render()
+        return @
+
+    ###
+    Flips an image
+    ###
+    @flip = (direction='horizontal')=>
+        @context.save()
+        if direction is 'horizontal'
+            @context.translate(@dimensions.w, 0);
+            @context.scale(-1,1)
+        else
+            @context.translate(0,@dimensions.h);
+            @context.scale(1,-1)
+
+        @context.drawImage(@target,0,0)
+        @context.restore()
+        @render()
+        return @
+
+    ###
+    Reflection
+    ###
+    @reflect = (alphaStart=0.5,gap=0,reflectionAmount=0.25,direction = 'vertical')=>
     @context.save()
     gradientCanvas = document.createElement('canvas')
     gradientCanvas.width = @imageDimensions.w
     gradientCanvas.height = @imageDimensions.h
-
+
     gradientContext = gradientCanvas.getContext('2d')
-
+
     @context.globalCompositeOperation = 'source-over'
-
-    startPos =
+
+    startPos =
       x:0
       y:0
-
+
     targetPos =
       x:0
       y:0
-
+
     {w,h} = @imageDimensions
     @context.drawImage(@target,0,0)
     if direction is 'vertical'
@@ -219,15 +219,15 @@
       @context.scale(1,-1)
       targetPos.y = @dimensions.h
       startPos.y = @imageDimensions.h-h
-
+
     gradientImageData = gradientContext.getImageData(0, 0, w, h);
-
+
     gradientContext.drawImage(@target,startPos.x,startPos.y,w,h,0,0,w,h)
-
+
     imageData = @context.getImageData(0,0,w,h)
     reflectionImageData = gradientContext.getImageData(0,0,w,h)
     opacity=1
-
+
     col = 1
     row = 1;
     alphaStep = (255*alphaStart)/h
@@ -239,31 +239,31 @@
         col++
       col=1
       row++
-
+
     @context.putImageData(reflectionImageData, 0,@imageDimensions.h)
-
+
     @imageDimensions = @dimensions
-
-
+
+
     @render()
     @context.restore()
     return @
-
-	#load element
-	@target = if target isnt null then document.querySelector(target) else document.createElement('canvas')
-	@canvas = document.createElement('canvas')
-	@context = @canvas.getContext('2d')
-
-	if img.match(fileRegExp)
-		@image = new Image()
-		@image.onload = =>
-			@setUpImageData()
-			@onLoad()
-		@image.src = img
-	else
-		@image = document.querySelector(img)
-		@setUpImageData()
-
-	return this
-
-root.obscura = obscura+
+    #load element
+    @target = if target isnt null then document.querySelector(target) else document.createElement('canvas')
+    @canvas = document.createElement('canvas')
+    @context = @canvas.getContext('2d')
+
+    if img.match(fileRegExp)
+        @image = new Image()
+        @image.onload = =>
+            @setUpImageData()
+            @onLoad()
+        @image.src = img
+    else
+        @image = document.querySelector(img)
+        @setUpImageData()
+
+    return this
+
+root.obscura = obscura


Get string's patterns.

Get string's patterns.

--- amCoffee/assets/scripts/main.coffee
+++ amCoffee/assets/scripts/main.coffee
@@ -128,7 +128,10 @@
             matches = source.match /(([a-z_$@0-9]+\.)*)[a-z_$@0-9]*$/i
             context = matches[1]
             if context is ''
-                context = 'window'
+                if matches = source.match /"(?:[^"\\]|\\.)*"\.[a-z_$@0-9]*$|'(?:[^'\\]|\\.)*'\.[a-z_$@0-9]*$/i
+                    context = 'new String("")'
+                else
+                    context = 'window'
             else
                 context = context.substr 0, context.length - 1


stub out query method

stub out query method

--- v1meta.coffee
+++ v1meta.coffee
@@ -4,7 +4,7 @@
 et = require('elementtree')
 util = require('util')
 url = require('url')
-
+
 asset_dict_filter = (dict) ->
     output = {}
     for k,v of dict
@@ -54,11 +54,11 @@
 class V1Transaction
     constructor: (@query_results=[], @v1meta) ->
         @dirty_assets = []
-
+
     add_to_dirty: (asset) ->
         if asset not in @dirty_asset
             @dirty_assets.push asset
-
+
     create: (asset_type, data) ->
         @v1meta.get_asset_class asset_type, (err, AssetClass) =>
             new_asset = new AssetClass(undefined, @)
@@ -73,19 +73,23 @@
         for dirty_asset in @dirty_assets
             @v1meta.update_asset dirty_asset, (err, update_result) =>
                 callback(err, dirty_asset, update_result)
-
+

 module.exports =
     V1Meta: class V1Meta
         constructor: (@server) ->
             @global_cache = {}

+        query: (asset_type_name, options) ->
+            q = new V1Query(this, asset_type_name, options)
+            q.execute()
+
         for_all_types: (callback) ->
             @server.get_meta_xml {asset_type_name: ''}, (err, meta_xml) =>
                 if not err?
                     meta_xml.iter 'AssetType', (asset_xml) =>
                         callback(@build_asset_class_from_xml(asset_xml))
-
+
         build_asset_class_from_xml: (xml) ->
             asset_type_name = xml.get('name')

@@ -160,7 +164,7 @@
                     trans.query_results = assets
                     callback(undefined, trans)

-        get_asset_class: (asset_type_name, callback) =>
+        get_asset_class: (asset_type_name, callback) =>
             if asset_type_name of @global_cache
                 callback(undefined, @global_cache[asset_type_name])
             else
@@ -168,10 +172,10 @@
                     return callback(error) if error?
                     cls = @build_asset_class_from_xml(xml)
                     @global_cache[asset_type_name] = cls
-                    callback(undefined, cls)
-
-
-
+                callback(undefined, cls)

+
+
+


Cleanup

Cleanup

--- api/apps/articles/model/save.coffee
+++ api/apps/articles/model/save.coffee
@@ -259,63 +259,11 @@
   ], (err, results) ->
     console.log results
     console.log err
+    debug err if err
     cb()

 postFacebookAPI = (article, cb) ->
-  html = "
-
-<meta charset=\"utf-8\">
-<meta property=\"op:markup_version\" content=\"v1.0\">
-<link rel=\"canonical\" href=\"https://www.artsy.net/article/artsy-editorial-stands-lose-nea-eliminated\">
-<meta property=\"fb:article_style\" content=\"default\">
-<meta property=\"fb:admins\" content=\"7961740\">
-
-
-

Who Stands to Lose the Most if the NEA Is Eliminated?

-
<p><br></p>

-                <address>Artsy Editorial</address>
-                <address>Isaac Kaplan</address>
-                <time datetime=\"2017-01-31T02:26:51.470Z\" class=\"op-published\">Jan 31st, 2017 2:26 am</time><time datetime=\"2017-01-31T02:26:51.469Z\" class=\"op-modified\">Jan 31st, 2017 2:26 am</time>

With an understanding that farmers and artists have creation in common, Reedsburg, Wisconsin’s Wormfarm formed a vision for farm-based ephemeral art installations and roadside Culture Stands. Photo via arts.gov.

-

Most Americans are less familiar with the National Endowment for the Arts’s mission than with its reputation as a source of controversy and Republican ire. The NEA has long been a target for elimination by Republicans, but they’ve never succeeded in wiping it out altogether. Until, perhaps, now. Pressing fears about the agency’s future arose after a report by The Hill revealed that an early Trump budget proposal is based heavily off plans drawn up by the right-leaning Heritage Foundation, which prescribes eliminating funding to the NEA and its sister agency the National Endowment for the Humanities. Several pieces have come out debunking the idea that cuts are fiscally necessary, given how little the NEA actually receives in government funding—about $147.9 million in fiscal year 2016, or 0.004% of the $3.9 trillion federal budget. Since the original report in The Hill, there hasn’t been much by the way of confirmation.

-
Regardless of the NEA’s future, it’s worth pausing to examine what it actually does and why it’s significant, beyond its role as one of the few dedicated government patrons of art organizations, education programs, exhibitions, festivals, and more. In response to the potential elimination of the agency, Australian artist Tega Brain created a running list of the projects it funded last year. On its relatively miniscule budget, the agency has an outsize impact, both through its grants programs as well as an indemnity program that saves museums millions of dollars. It helps arts organizations raise private funds, and its geographical mandate ensures that funds reach beyond the nation’s cultural capitals. Here are three key things to know about the NEA.

-
Geographical Distribution

-
A crucial aspect of the NEA is that it is a national organization with a mission to “promote equal access to the arts in every community across America,” according to its website. This means the agency’s impact reaches beyond just cultural capitals or the coasts. Instead, NEA “funds are geographically distributed so that they’re touching counties, states, communities throughout the country,” says Zannie Voss, director of the National Center for Arts Research (NCAR). “There’s no other support source that is able or in a position to provide that kind of national elevation of arts and culture.”

-
According to the NEA, it awarded over 2,500 grants this past fiscal year, spread across all 435 congressional districts. Smaller organizations, defined as those who spent less than $350,000 the prior year—received 30% of NEA direct grants. Major institutions, those that spent over $1.75 million, got slightly more, at 35% of all direct grants, and mid-sized institutions got the other 35%. So while large institutions in big cities would likely be impacted by cuts, “there’s a lot more funders in New York, in L.A., in Chicago, who can pick up the mantle,” said Meg Leary of United States Artists (USA), an independent private arts nonprofit that provides grants directly to artists—something the NEA hasn’t done, except to writers, since the bruising culture wars of the ’90s. “It’s the rural areas of the country, it’s places that have not that much access to cultural resources already who will be incredibly affected,” she added. Along with more private donors, big cities also have stronger public arts funding as well. For example, New York City’s cultural agency would be the largest public funder of the arts in the entire nation if the NEA were to be eliminated.

-
The NEA also often awards its funding to “economically diverse communities,” meaning they have a high percentage of households that are wealthy and of those that are below the poverty line. And while absolute NEA grant dollars rise in accordance with population size (bigger cities get more money overall), the actual per capita value of NEA grants increases as population lessens (smaller communities get more NEA dollars per person).

-
Straddling the Public / Private Divide

-
Often the debate over the NEA implies a clear distinction between public money and private money. The latter is abundant, the former is scarce—so why do we need it at all? In fact, those meager public dollars have a multiplier effect because of how the NEA operates, enabling arts organizations to raise lots more of the same private dollars that groups like the Heritage Foundation argue make the NEA superfluous. So yes, the average American taxpayer gives the NEA less than pennies on the dollar per year while total private arts funding amounted to about $42 dollars per person in 2012. But because the NEA puts grantees through a rigorous vetting process before doling out a funds, recipients are more likely to receive further funding once they’ve secured the NEA imprimatur.

-
In 2016, the NEA allocated about $71 million for its grant and awards program. So how does an exhibition organizer or an arts nonprofit go about getting an NEA grant? Unsurprisingly, it begins with a grant application. We won’t bore you with too many details, but these applications are reviewed by some 75 panels convened annually and staffed by professionals from a specific discipline (arts education, museums, folk and traditional arts, etc.) along with at least one lay person. The panels’ recommendations are then sent to the National Council on the Arts, which is comprised of artists, scholars, arts patrons—appointed by the president and approved by the Senate—as well as six non-voting members of Congress. After the NCA weighs in, the final recommendations head to the NEA chair, currently Jane Chu, for final review.

-
So if you get one of the thousands of grants awarded each year, “it’s a statement saying that you’ve made it,” which in turn helps raise more funds privately, said Leary. Often, NEA grants are also conditional on raising matching funds from other sources for the amount of the grant. According to the agency, every $1 of public money resulted in $9 of private and other public dollars in 2016. The NEA calculates that grants resulted in arts organizations bringing in roughly $500 million in such funds in 2016.

-
States, Diversity, and Indemnity

-
State arts agencies are also the recipient of federal funding through the NEA, which gives about 40% of its grant and awards budget, around $47 million, to the states (although state-level arts agencies themselves have been targeted by local Republicans for elimination). State money then sometimes trickles down to local arts authorities as well. Significantly, the NEA runs a National Heritage Fellowship program geared towards folk and traditional arts and artists, which provides over $4 million annually in grants and awards.

-
The NEA also administers an important federal indemnity program that allows museums to collectively save $30 million each year by offsetting insurance costs. Created in the 1970s, the program has only paid out two claims, totaling $104,700, while helping museums put on expansive exhibitions they might not be able to afford if they had to pay for private insurance for each and every work. It’s been used by numerous institutions to ease their financial burden this year alone, including by the Philadelphia Museum of Art’s “International Pop” show and the Taft Museum of Art’s “Daubigny, Monet, Van Gogh: Impressions of Landscape” exhibition.

-
Beyond all of these important measures, the NEA’s elimination would be a deeply symbolic loss. Today there is, in this country, a national arts agency that rewards and fosters the culture and creativity necessary for any healthy democracy. As the Washington Post’s culture critic Philip Kennicott argued, what’s at stake is the very notion of a democratic space in which art, ideas, and information are produced beyond the constraints of commerce. The attack on the NEA is part, he writes, “of a nascent but ominous larger movement to eliminate the last vestiges of a public realm free of the dictates of the market.”

-
—Isaac Kaplan

-

-

-
-

-
-"
+  html = ''
   request
     .post "https://graph.facebook.com/v2.7/#{FB_PAGE_ID}/instant_articles"
     .send
@@ -323,10 +271,10 @@
       development_mode: true
       html_source: html
     .end (err, response) =>
-      console.log response
-      cb()
-
-postSailthruAPI = (article, tags, images, cbAPI) ->
+      return cb err if err
+      cb response
+
+postSailthruAPI = (article, tags, images, cb) ->
   html = if article.send_body then getTextSections(article) else ''
   sailthru.apiPost 'content',
   url: "#{FORCE_URL}/article/#{_.last(article.slugs)}"
@@ -344,8 +292,8 @@
     daily_email: article.daily_email
     weekly_email: article.weekly_email
   , (err, response) =>
-    debug err if err
-    cbAPI()
+    return cb err if err
+    cb response

 cleanArticlesInSailthru = (slugs = []) =>
   if slugs.length > 1


Fixed: the check to change today's date was done only once. Refs #100

--- client/app/lib/today_checker.coffee
+++ client/app/lib/today_checker.coffee
@@ -1,13 +1,20 @@
 # For people that let their computer / browser run during the night
-# the 'today' is not properly set anymore the day after
+# the 'today' is not properly set anymore the day after.
 module.exports = (router) ->

-    now = moment()
-    nextDay = moment(now).add(1, 'days').startOf 'day'
-    nextTick = nextDay.valueOf() - now.valueOf()
-    setTimeout ->
-        # completely re-render fullCalendar if it's open
-        # fullCalendar doesn't allow to change the 'today' date manually
-        view = router.mainView
-        view.cal.fullCalendar 'render' if view.cal?
-    , nextTick
+    # Define `waitToChangeToday`, and run it.
+    do waitToChangeToday = =>
+
+        now = moment()
+        nextDay = moment(now).add(1, 'days').startOf 'day'
+        nextTick = nextDay.valueOf() - now.valueOf()
+        setTimeout ->
+            # Completely re-render fullCalendar if it's open.
+            # fullCalendar doesn't allow to change the 'today' date manually.
+            view = router.mainView
+            view.cal.fullCalendar 'render' if view.cal?
+
+            # Restart the timer for the next day.
+            waitToChangeToday()
+
+        , nextTick


Login: resize handler and transtion handler removed

--- client/landing/Login/AppView.coffee
+++ client/landing/Login/AppView.coffee
@@ -126,26 +126,20 @@

   viewAppended:->

-    @setY -KD.getSingleton('windowController').winHeight
-    @listenWindowResize()
     @setClass "login-screen login"

     @setTemplate @pistachio()
     @template.update()

-  _windowDidResize:->
-    if @hidden
-      @setY -KD.getSingleton('windowController').winHeight
-
   pistachio:->
+      # {{> @loginOptions}}
+      # {{> @registerOptions}}
     """

         beta
         {{> @logo}}

-      {{> @loginOptions}}
-      {{> @registerOptions}}

         {{> @loginForm}}


src/contextFactory commented

src/contextFactory commented

--- src/contextFactory.coffee
+++ src/contextFactory.coffee
@@ -1,28 +1,24 @@
-define 'contextFactory', ()->
-  (prop)->
+# Temporarily not in use
+#
+# define 'contextFactory', ['store', 'jasmine', 'privateStore'], (_Store_, _Jasmine_, _Context_)->
+#   (prop, Store = _Store_, Jasmine = _Jasmine_, Context = _Context_)->
+#     return {} unless Jasmine.defined()

-    @is = (fn)->
-      return unless fn?
-      @defined = true
-      origin   = this
-      console.log 'in factory'
-      try
-        res = fn()
-      catch e
-        console.log e.name
-        console.log e.message
-      console.log res
-      # console.log collection
-      # console.log collection
-      # console.log _collection_
+#     @is = (fn)->
+#       return unless fn?
+#       @defined    = true
+#       Store[prop] = fn

-      Jasmine.instance.beforeEach ->
-        console.log 'in beforeEach'
-        eval("#{prop} = res;")
-      # Jasmine.instance.afterEach ->
+#       Jasmine.instance.beforeEach.call Context.get(), ->
+#         this[prop] = fn.call this
+#         eval("#{prop} = this.#{prop};")

-    @value = -> fn()
+#       Jasmine.instance.afterEach.call Context.get(), ->
+#         delete Store[prop]
+#         eval "delete #{prop};"

-    @defined = false
+#     @value = -> Store[prop]

-    this
+#     @defined = false
+
+#     this


Bryans victory x2, actually everything works. fixed trying to set _id

--- server/GrandCentral.coffee
+++ server/GrandCentral.coffee
@@ -1,33 +1,29 @@
+_debug = true
+
 class GrandCentral
   constructor: (@collection, @method) ->
+    @default = Meteor.default_server.method_handlers["/#{@collection}/#{@method}"]
     Meteor.default_server.method_handlers["/#{@collection}/#{@method}"] = @dispatch
   dispatch: (args...) =>
     {
       users: {
         insert: (args...) =>
-          Users.insert(args...)
         update: (args...) =>
-          Users.update(args...)
         remove: (args...) =>
-          Users.remove(args...)
       }
       posts: {
-        insert: (args...) =>
-          Posts.insert(args...)
+        insert: (args...) =>
+          args[0].author_id = Session.get 'user_id'
         update: (args...) ->
-          Posts.update(args...)
         remove: (args...) =>
-          Posts.remove(args...)
       }
       instances: {
         insert: (args...) =>
-          Instances.insert(args...)
         update: (args...) =>
-          Instances.update(args...)
         remove: (args...) =>
-          Instances.remove(args...)
       }
     }[@collection][@method](args...)
+    @default.apply(@, args)
     console.log "GrandCentral is operational!!"


Prevent filler node calculation from being performed multiple times

Fixes #37, #33

--- src/d3-flame-graph.coffee
+++ src/d3-flame-graph.coffee
@@ -21,20 +21,20 @@
     if maxHash > 0 then result / maxHash else result

   addFillerNodes = (node) ->
-    # TODO: if there are no fillers this can be expensive as it's called often
     children = node.children
-    return node if not children?.length
-    return node if children.filter((child) -> child.filler).length > 0
+    return node if node.filled or not children?.length
     childSum = children.reduce ((sum, child) -> sum + child.value), 0
     if childSum < node.value
       children.push
         value: node.value - childSum
         filler: true
     children.forEach(addFillerNodes)
+    node.filled = true
     node

   # augments each node in the tree with the maximum distance
   # it is from a terminal node
+  # TODO: this should be done in the same pass as the filler addition
   addMaxDepth = (node) ->
     computeDepth = (node) ->
       return 0 if not node # TODO: this should not be needed
@@ -92,6 +92,7 @@

     zoom: (node) ->
       throw new Error("Zoom is disabled!") if not @zoomEnabled()
+      @tip.hide()
       if node in @_ancestors
         @_ancestors = @_ancestors.slice(0, @_ancestors.indexOf(node))
       else


ManagedVMs: Code refactor.

ManagedVMs: Code refactor.

--- client/app/lib/providers/managed/addmanagedmachinemodal.coffee
+++ client/app/lib/providers/managed/addmanagedmachinemodal.coffee
@@ -60,6 +60,7 @@
     { computeController } = kd.singletons

     computeController.ready =>
+
       computeController.fetchPlanCombo 'managed', (err, userPlanInfo) =>

         return @handleError err  if err
@@ -68,35 +69,42 @@
         limit = plans[plan].managed
         used  = usage.total

-        if used >= limit
-          return @handleUsageLimit()
+        return @handleUsageLimit()  if used >= limit

         whoami().fetchOtaToken (err, token) =>
           return @handleError err  if err

-          kontrolUrl = if globals.config.environment in ['dev', 'sandbox']
-          then "export KONTROLURL=#{globals.config.newkontrol.url}; "
-          else ''
+          @updateContentViews token

-          cmd = "#{kontrolUrl}curl -sL https://kodi.ng/s | bash -s #{token}"

-          @loader.destroy()
-          @code.addSubView @input = new kd.InputView
-            defaultValue : cmd
-            click        : =>
-              @showTooltip()
-              @input.selectAll()
+  updateContentViews: (token) ->

-          @code.addSubView @selectButton = new kd.CustomHTMLView
-            cssClass : 'select-all'
-            partial  : 'SELECT'
-            click    : =>
-              @showTooltip()
-              @input.selectAll()
+    kontrolUrl = if globals.config.environment in ['dev', 'sandbox']
+    then "export KONTROLURL=#{globals.config.newkontrol.url}; "
+    else ''

-          computeController.managedKiteChecker.addListener @bound 'machineFoundCallback'
+    cmd = "#{kontrolUrl}curl -sL https://kodi.ng/s | bash -s #{token}"

-          @createPollLoader()
+    @loader.destroy()
+    @code.addSubView @input = new kd.InputView
+      defaultValue : cmd
+      click        : =>
+        @showTooltip()
+        @input.selectAll()
+
+    @code.addSubView @selectButton = new kd.CustomHTMLView
+      cssClass : 'select-all'
+      partial  : 'SELECT'
+      click    : =>
+        @showTooltip()
+        @input.selectAll()
+
+    { computeController } = kd.singletons
+    computeController.managedKiteChecker.addListener @bound 'machineFoundCallback'
+
+    kd.utils.wait 20000, =>
+      @addSubView new kd.LoaderView showLoader: yes, size: width: 26
+      @setClass 'polling'

   showTooltip: ->
@@ -138,13 +146,6 @@
     @setClass 'error'

-  createPollLoader: ->
-
-    kd.utils.wait 20000, =>
-      @addSubView new kd.LoaderView showLoader: yes, size: width: 26
-      @setClass 'polling'
-
-
   destroy: ->

     super


AddManagedMachineModal: fix KONTROL_URL export

--- client/app/lib/providers/managed/addmanagedmachinemodal.coffee
+++ client/app/lib/providers/managed/addmanagedmachinemodal.coffee
@@ -96,7 +96,7 @@

     if environment is 'production' and \
     location.hostname.indexOf("latest.#{globals.config.domains.base}") > -1
-      kontrolUrl = "export KONTROLURL=#{KodingKontrol.getKontrolUrl()} "
+      kontrolUrl = "export KONTROLURL=#{KodingKontrol.getKontrolUrl()}; "
     else
       kontrolUrl = if environment in ['dev', 'default', 'sandbox']
       then "export KONTROLURL=#{KodingKontrol.getKontrolUrl()} CHANNEL=devmanaged; "


Finder: Code styling.

Finder: Code styling.

--- client/finder/filetree/controllers/findertreecontroller.coffee
+++ client/finder/filetree/controllers/findertreecontroller.coffee
@@ -426,38 +426,36 @@
   CONTEXT MENU OPERATIONS
   ###

-  cmExpand:        (nodeView, contextMenuItem)-> @expandFolder node for node in @selectedNodes
-  cmCollapse:      (nodeView, contextMenuItem)-> @collapseFolder node for node in @selectedNodes # error fix this
-  cmMakeTopFolder: (nodeView, contextMenuItem)-> @makeTopFolder nodeView
-  cmRefresh:       (nodeView, contextMenuItem)-> @refreshFolder nodeView
-  cmToggleDotFiles:(nodeView, contextMenuItem)-> @toggleDotFiles nodeView
-  cmResetVm:       (nodeView, contextMenuItem)-> @resetVm nodeView
-  cmUnmountVm:     (nodeView, contextMenuItem)-> @unmountVm nodeView
   cmOpenVmTerminal:(nodeView, contextMenuItem)-> @openVmTerminal nodeView
-  cmCreateFile:    (nodeView, contextMenuItem)-> @createFile nodeView
-  cmCreateFolder:  (nodeView, contextMenuItem)-> @createFile nodeView, "folder"
-  cmRename:        (nodeView, contextMenuItem)-> @showRenameDialog nodeView
-  cmDelete:        (nodeView, contextMenuItem)-> @confirmDelete nodeView
-  cmDuplicate:     (nodeView, contextMenuItem)-> @duplicateFiles @selectedNodes
-  cmExtract:       (nodeView, contextMenuItem)-> @extractFiles nodeView
-  cmZip:           (nodeView, contextMenuItem)-> @compressFiles nodeView, "zip"
-  cmTarball:       (nodeView, contextMenuItem)-> @compressFiles nodeView, "tar.gz"
-  cmUpload:        (nodeView, contextMenuItem)-> @uploadFile nodeView
-  cmDownload:      (nodeView, contextMenuItem)-> @appManager.notify()
-  cmGitHubClone:   (nodeView, contextMenuItem)-> @appManager.notify()
-  cmOpenFile:      (nodeView, contextMenuItem)-> @openFile nodeView
-  cmPreviewFile:   (nodeView, contextMenuItem)-> @previewFile nodeView
-  cmCompile:       (nodeView, contextMenuItem)-> @compileApp nodeView
-  cmMakeNewApp:    (nodeView, contextMenuItem)-> @makeNewApp nodeView
-  cmPublish:       (nodeView, contextMenuItem)-> @publishApp nodeView
-  cmOpenFileWithApp: (nodeView, contextMenuItem)-> @openFileWithApp  nodeView, contextMenuItem
-  cmCloneRepo:     (nodeView, contextMenuItem)-> @cloneRepo nodeView
-  cmDropboxChooser:(nodeView, contextMenuItem)-> @chooseFromDropbox nodeView
-  cmOpenTerminal:  (nodeView, contextMenuItem)-> @openTerminalFromHere nodeView
-  # cmShowOpenWithModal: (nodeView, contextMenuItem)-> @showOpenWithModal nodeView
-  # cmOpenFileWithApp: (nodeView, contextMenuItem)-> @openFileWithApp  nodeView, contextMenuItem
-
-  cmOpenFileWithCodeMirror:(nodeView, contextMenuItem)-> @appManager.notify()
+  cmExpand:              (node) -> @expandFolder         node for node in @selectedNodes
+  cmCollapse:            (node) -> @collapseFolder       node for node in @selectedNodes # error fix this
+  cmMakeTopFolder:       (node) -> @makeTopFolder        node
+  cmRefresh:             (node) -> @refreshFolder        node
+  cmToggleDotFiles:      (node) -> @toggleDotFiles       node
+  cmResetVm:             (node) -> @resetVm              node
+  cmUnmountVm:           (node) -> @unmountVm            node
+  cmCreateFile:          (node) -> @createFile           node
+  cmCreateFolder:        (node) -> @createFile           node, "folder"
+  cmRename:              (node) -> @showRenameDialog     node
+  cmDelete:              (node) -> @confirmDelete        node
+  cmExtract:             (node) -> @extractFiles         node
+  cmZip:                 (node) -> @compressFiles        node, "zip"
+  cmTarball:             (node) -> @compressFiles        node, "tar.gz"
+  cmUpload:              (node) -> @uploadFile           node
+  cmOpenFile:            (node) -> @openFile             node
+  cmPreviewFile:         (node) -> @previewFile          node
+  cmCompile:             (node) -> @compileApp           node
+  cmMakeNewApp:          (node) -> @makeNewApp           node
+  cmPublish:             (node) -> @publishApp           node
+  cmCloneRepo:           (node) -> @cloneRepo            node
+  cmDropboxChooser:      (node) -> @chooseFromDropbox    node
+  cmOpenTerminal:        (node) -> @openTerminalFromHere node
+  cmDuplicate:           (node) -> @duplicateFiles       @selectedNodes
+  cmDownload:            (node) -> @appManager.notify()
+  cmGitHubClone:         (node) -> @appManager.notify()
+  # cmOpenFileWithApp:     (node, contextMenuItem) -> @openFileWithApp  node, contextMenuItem
+  # cmShowOpenWithModal: (node, contextMenuItem) -> @showOpenWithModal node
+  # cmOpenFileWithApp:   (node, contextMenuItem) -> @openFileWithApp  node, contextMenuItem

   ###
   CONTEXT MENU CREATE/MANAGE


change newline code

change newline code

--- coffee/backbone/views/controls/rotate_axis_control.coffee
+++ coffee/backbone/views/controls/rotate_axis_control.coffee
@@ -1,79 +1,86 @@
-#
-# TODO better class name
-#
-class @RotateAxisControl extends Backbone.View
-    events: () ->
-        "mousedown": "onMouseDown"
-
-    initialize: () ->
-        @rotate_point_el = @options.rotate_point_el
-        @getItem = @options.get_item
-        @setStyle()
-        @rotate_point_view = new RotatePointControl(el:@rotate_point_el)
-        @rotate_point_view.bind("change", @render)
-
-    setStyle:()->
-        $(@el).attr({
-            "stroke" : "black",
-            "stroke-width" : "1",
-            "fill" : "lightblue",
-            "r" : "8"
-        })
-
-    getRotateAxisPoint:() =>
-        @rotate_point_view.point
-
-    onMouseDown:(e) =>
-        @trigger("onMouseDown", @, e)
-        @_origin_matrix = @getItem().getLocalMatrix()
-        @_origin_rotate_point = @getRotateAxisPoint().matrixTransform(SvgCanvasBase.mainCanvas.getScreenCTM()) #TODO global
-
-        center = @getItem().getCentorPoint()
-        @_origin_degree = @_radianToDegree(@_radian(center, @_origin_rotate_point))
-
-    onDragging:(e)=>
-        mouse_point =SVGUtil.createPoint(e.pageX, e.pageY)
-        degree = @_radianToDegree(@_radian(mouse_point, @_origin_rotate_point))
-        rot_degree = degree
-
-        snap_degree = 15
-        if e.shiftKey
-            div = parseInt(degree / snap_degree)
-            for p, idx in [(snap_degree * div), (snap_degree * (div - 1))]
-              if(Math.abs(p - degree) < 10)
-                rot_degree = p
-
-        @setRotate(rot_degree, @rotatePoint())
-
-    rotatePoint: () =>
-        @getRotateAxisPoint().matrixTransform(@_origin_matrix.inverse())
-
-    setRotate: (angle, point) =>
-        transform = d3.transform("matrix(" + SVGUtil.toStringMatrix(@_origin_matrix.translate(point.x, point.y))+ " )")
-        transform.rotate = transform.rotate+angle-@_origin_degree
-        matrix = SVGUtil.TransformMatrix(transform.toString()).translate(-point.x, -point.y)
-        @getItem().setMatrix(matrix)
-
-    _radian: (p, origin) ->
-        Math.atan2(p.y - origin.y, p.x - origin.x)
-
-    _radianToDegree:(radian) ->
-        (radian * (180/Math.PI)) + 90
-
-    render:() =>
-        @render_control_point()
-        @render_rotate_point()
-
-    render_control_point:() =>
-        rotate_point = @getRotateAxisPoint().matrixTransform(SvgCanvasBase.mainCanvas.getScreenCTM())
-        center = @getItem().getCentorPoint()
-        r = Math.atan2(center.y - rotate_point.y, center.x - rotate_point.x)
-        y = Math.sin(r) * 100
-        x = Math.cos(r) * 100
-        element = $(@el)
-        rotate_point = @getRotateAxisPoint().matrixTransform(SvgCanvasBase.mainCanvas.getCTM())
-        element.attr("cx", rotate_point.x  +  x)
-        element.attr("cy", rotate_point.y  +  y)
-
-    render_rotate_point:() =>
-        @rotate_point_view.render()
+#
+# TODO better class name
+#
+class @RotateAxisControl extends Backbone.View
+    events: () ->
+        "mousedown": "onMouseDown"
+
+    initialize: () ->
+        @rotate_point_el = @options.rotate_point_el
+        @getItem = @options.get_item
+        @setStyle()
+        @rotate_point_view = new RotatePointControl(el:@rotate_point_el)
+        @rotate_point_view.bind("change", @render)
+
+    setStyle:()->
+        $(@el).attr({
+            "stroke" : "black",
+            "stroke-width" : "1",
+            "fill" : "lightblue",
+            "r" : "8"
+        })
+
+    getRotateAxisPoint:() =>
+        @rotate_point_view.point
+
+    onMouseDown:(e) =>
+        @trigger("onMouseDown", @, e)
+        @_origin_matrix = @getItem().getLocalMatrix()
+        @_origin_rotate_point = @getRotateAxisPoint().matrixTransform(SvgCanvasBase.mainCanvas.getScreenCTM()) #TODO global
+
+        center = @getItem().getCentorPoint()
+        @_origin_degree = @_radianToDegree(@_radian(center, @_origin_rotate_point))
+
+    onDragging:(e)=>
+        mouse_point = SVGUtil.createPoint(e.pageX, e.pageY)
+        degree = @_radianToDegree(@_radian(mouse_point, @_origin_rotate_point))
+        rot_degree = degree
+
+        snap_degree = 15
+        if e.shiftKey
+            div = parseInt(degree / snap_degree)
+            for p, idx in [(snap_degree * div), (snap_degree * (div - 1))]
+              if(Math.abs(p - degree) < 10)
+                rot_degree = p
+
+        @setRotate(rot_degree, @rotatePoint())
+
+    rotatePoint: () =>
+        @getRotateAxisPoint().matrixTransform(@_origin_matrix.inverse())
+
+    setRotate: (angle, point) =>
+        transform = d3.transform("matrix(" + SVGUtil.toStringMatrix(@_origin_matrix.translate(point.x, point.y))+ " )")
+        transform.rotate = transform.rotate+angle-@_origin_degree
+        matrix = SVGUtil.TransformMatrix(transform.toString()).translate(-point.x, -point.y)
+        # matrix = @_origin_matrix.translate(point.x, point.y)
+        #          .rotate(angle-@_origin_degree)
+        #          .translate(-point.x, -point.y)
+        @getItem().setMatrix(matrix)
+
+    _radian: (p, origin) ->
+        Math.atan2(p.y - origin.y, p.x - origin.x)
+
+    _radianToDegree:(radian) ->
+        (radian * (180/Math.PI)) + 90
+
+    render:() =>
+        @render_control_point()
+        @render_rotate_point()
+
+    render_control_point:() =>
+
+        canvas = SvgCanvasBase.mainCanvas
+        screen_rotate_point = @getRotateAxisPoint().matrixTransform(canvas.getScreenCTM())
+        center = @getItem().getCentorPoint()
+        r = Math.atan2(center.y - screen_rotate_point.y,
+                       center.x - screen_rotate_point.x)
+
+        x = Math.cos(r) * 100
+        y = Math.sin(r) * 100
+
+        rotate_point = @getRotateAxisPoint().matrixTransform(canvas.getCTM())
+        @$el.attr("cx", rotate_point.x  +  x)
+        @$el.attr("cy", rotate_point.y  +  y)
+
+    render_rotate_point:() =>
+        @rotate_point_view.render()


Update spec with nulls

Update spec with nulls

--- spec/javascripts/unit/darkswarm/services/map_spec.js.coffee
+++ spec/javascripts/unit/darkswarm/services/map_spec.js.coffee
@@ -18,7 +18,8 @@
       orders_close_at: new Date()
       type: "hub"
       visible: true
-
+      latitude: null
+      longitude: null
     }
   ]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com


Implement directInput handling

Implement directInput handling

--- src/dredd.coffee
+++ src/dredd.coffee
@@ -34,9 +34,23 @@
     config = @configuration
     stats = @stats

+    configDataIsEmpty = true
+
     config.files = []
     config.data = {}
     runtimes = {}
+
+    passedConfigData = {}
+
+    for own key, val of config.directInput or {}
+      configDataIsEmpty = false
+      passedConfigData[key] = {
+        filename: key
+        raw: val
+      }
+
+    if not configDataIsEmpty
+      config.data = passedConfigData

     # expand all globs
     expandGlobs = (cb) ->
@@ -48,7 +62,9 @@

       , (err) =>
         return callback(err, stats) if err
-        return callback({message: "Blueprint file or files not found on path: '#{config.options.path}'"}, stats) if config.files.length == 0
+
+        if configDataIsEmpty and config.files.length == 0
+          return callback({message: "Blueprint file or files not found on path: '#{config.options.path}'"}, stats)

         # remove duplicate filenames
         config.files = config.files.filter (item, pos) ->


add usefull methods in Utils module

add usefull methods in Utils module


:triangular_ruler: adding utils.pointInPolygon


Remove the gallery insights blog

Remove the gallery insights blog

This is now over on a WordPress blog so we're just killing this route
and we've setup a vanity shortcut for it.

Co-authored-by: Eve Essex <e03bfd8c7d84707576f031bff3a684e37573ae80@gmail.com>

--- src/desktop/config.coffee
+++ src/desktop/config.coffee
@@ -127,7 +127,7 @@
   SITEMAP_BASE_URL: 'http://artsy-sitemaps.s3-website-us-east-1.amazonaws.com'
   SHOW_ANALYTICS_CALLS: false
   STRIPE_PUBLISHABLE_KEY: null
-  TEAM_BLOGS: '^\/life-at-artsy$|^\/artsy-education$|^\/gallery-insights$|^\/buying-with-artsy$'
+  TEAM_BLOGS: '^\/life-at-artsy$|^\/artsy-education$|^\/buying-with-artsy$'
   TARGET_CAMPAIGN_URL: '/seattle-art-fair-2017'
   TRACK_PAGELOAD_PATHS: null
   TWILIO_ACCOUNT_SID: null


Do not show the deleted proposals

Do not show the deleted proposals

--- server/publications.coffee
+++ server/publications.coffee
@@ -75,6 +75,7 @@
 Meteor.publish "proposals", (query, options) ->
   options = {} if not options
   query = {} if not query
+  query = _.extend(query, notDeletedPred)
   # if not query.id
   #   query = _.extend(query, {status: 'accepted'})
   proposals = Proposal.find(query, _.extend(options, {fields: proposalFields(@userId)}))


Rename the normalise_coefficients function.

--- js/inequality.coffee
+++ js/inequality.coffee
@@ -81,7 +81,7 @@
   throw "Parse error at '#{input}' (tokens: #{tokens}, last token: #{previous})"

 # Replace all subtract operators with adds, normalising the coefficeints
-normalise_coefficients = (tokens, normalised=[]) ->
+convert_operators = (tokens, normalised=[]) ->
   if tokens.length == 0
     return normalised

@@ -92,12 +92,12 @@
     if tokens[1].type == "COEFFICIENT"
       value = parseFloat(tokens[1].value) * -1
       coefficient = new Token "COEFFICIENT", "#{value}"
-      return normalise_coefficients tokens.slice(2), normalised.concat(operator, coefficient)
+      return convert_operators tokens.slice(2), normalised.concat(operator, coefficient)
     else
       coefficient = new Token "COEFFICIENT", "-1"
-      return normalise_coefficients tokens.slice(1), normalised.concat(operator, coefficient)
+      return convert_operators tokens.slice(1), normalised.concat(operator, coefficient)

-  return normalise_coefficients tokens.slice(1), normalised.concat(tokens[0])
+  return convert_operators tokens.slice(1), normalised.concat(tokens[0])

 # Parse the lexed tokens into a set of terms
 parse = (tokens, terms=[]) ->
@@ -134,10 +134,10 @@
   tokens = lex i
   console.log tokens

-  normalised = normalise_coefficients tokens
-  console.log normalised
+  converted = convert_operators tokens
+  console.log converted

-  result = parse normalised
+  result = parse converted
   console.log result.join(" ")

 # These should be invalid.


privatemessage replyview fixed

privatemessage replyview fixed

--- client/landing/app/Applications/Inbox.kdapplication/Views/InboxReplyForm.coffee
+++ client/landing/app/Applications/Inbox.kdapplication/Views/InboxReplyForm.coffee
@@ -21,6 +21,6 @@
     @addSubView @sendButton = new KDButtonView
       title     : "Send"
       style     : "clean-gray inside-button"
-      callback  : @commentInputReceivedEnter
+      callback  : @bound "commentInputReceivedEnter"

     @attachListeners()


Guard against global project being null

Guard against global project being null

--- src/root-view.coffee
+++ src/root-view.coffee
@@ -280,5 +280,5 @@
   # Private: Destroys everything.
   remove: ->
     editor.remove() for editor in @getEditors()
-    project.destroy()
+    project?.destroy()
     super


Removed debugging message from uploader

Removed debugging message from uploader

--- js/controls/uploader/uploader.coffee
+++ js/controls/uploader/uploader.coffee
@@ -77,7 +77,6 @@
     @setStyleOfPart( 'form', 'visibility', 'hidden' )

   setUploadState: (_state, _uploadKey)->
-    console.log('state:',_state,', uploadKey:',_uploadKey)
     if _state != @uploadState
       @uploadState = _state
       _stateKey = _state.toString()


Hide skeletons boxes at start

Hide skeletons boxes at start

--- src/client/lib/01_graphics/01_skeleton_visualization.coffee
+++ src/client/lib/01_graphics/01_skeleton_visualization.coffee
@@ -63,6 +63,7 @@
     @_boxes = {}
     for joint in JOINTS
       box = new THREE.Mesh geometry, @_boxOffMaterial
+      box.visible = false
       @_boxes[joint] = box

   _initLines: ->


Add paging support

Add paging support

--- src/components/WebService/Basecamp/BasecampComponent.coffee
+++ src/components/WebService/Basecamp/BasecampComponent.coffee
@@ -25,19 +25,28 @@
         Authorization: @prepareAuth token
         Host: host

-    get: (path, success) ->
+    get: (path, success, page = null) ->
         options =
             host: @hostname
             port: 443
             path: path
             headers: @prepareHeaders @hostname, @apikey
+
+        options.path = "#{path}?page=#{page}" if page
+        page = 1 unless page
+        component = this
+
         req = https.get options, (resp) ->
             resp.setEncoding "utf8"
             body = ""
             resp.on "data", (data) ->
                 body += data
             resp.on "end", ->
-                success(body)
+                success body
+
+                # See if there are more pages to traverse
+                if resp.headers['x-pages'] > page
+                    component.get path, success, page + 1

     parse: (data, success) ->
         parser = new xml2js.Parser


Drop need for boilerplate CoffeeScript constructor.


redirect on success

redirect on success

--- src/donations-form.coffee
+++ src/donations-form.coffee
@@ -442,6 +442,8 @@
       pusher.disconnect()
       if data.status == "success"
         $("#donation-script").trigger("donations:success")
+        if config['redirectto']?
+          window.location.replace(config['redirectto'])
         donationsForm.hide()
         $(".donations-callback-flash").show(0).delay(8000).hide(0)
       else


Improve spec description

Improve spec description

--- spec/api-crash-reporter-spec.coffee
+++ spec/api-crash-reporter-spec.coffee
@@ -61,6 +61,6 @@
       w.loadURL url

   describe ".start(options)", ->
-    it 'requires that the companyName and submitURL option fields be specified', ->
+    it 'requires that the companyName and submitURL options be specified', ->
       assert.throws(-> crashReporter.start({companyName: 'Missing submitURL'}))
       assert.throws(-> crashReporter.start({submitURL: 'Missing companyName'}))


Add React package to test file

Add React package to test file

--- spec/beautify-languages-spec.coffee
+++ spec/beautify-languages-spec.coffee
@@ -54,6 +54,7 @@
   dependentPackages = [
     'autocomplete-plus'
     'fuse'
+    'react'
     # 'linter'
     #   'atom-typescript' # it logs too much...
   ]


#10 Turbolinks Incompatibly

#10 Turbolinks Incompatibly

--- lib/generators/hierapolis/templates/assets/javascripts/hq/application.js.coffee
+++ lib/generators/hierapolis/templates/assets/javascripts/hq/application.js.coffee
@@ -14,4 +14,10 @@
 #= require jquery_ujs
 #= require turbolinks
 #= require bootstrap
-#= require hierapolis+#= require hierapolis
+
+ready = ->
+  # TODO
+
+$(document).ready(ready)
+$(document).on('page:load', ready)


action log: expand logs

action log: expand logs

--- lib/actions/views/logs.coffee
+++ lib/actions/views/logs.coffee
@@ -155,15 +155,19 @@
       renderLogList data, data.logs, logs_dest, level + 1, ignore_date
       logs_dest.show()

-expandLogs = () ->
-  expandedOne = false
-  for i in $(".showLogs")
+expandLogs = (root="body", list = null) ->
+  if !list
+    list = $(".showLogs", root)
+  rest = list[101..-1]
+  if rest.length > 0
+    setTimeout (->
+      expandLogs root, rest), 10
+  for i in list[0..100]
     button = $(i)
     if button.text() == "[+]"
       button.trigger("click")
-      expandedOne = true
-  if expandedOne
-    setTimeout expandLogs, 20
+      setTimeout (->
+        expandLogs i), 10

 this.showMore = (dest="body") ->
   for i in $(".showMore", dest)


Remove unnecessary indirection

Remove unnecessary indirection

--- lib/changes/file-summary-view.coffee
+++ lib/changes/file-summary-view.coffee
@@ -27,9 +27,7 @@
     @buttonNode   = @querySelector(".btn")

   attachedCallback: ->
-    @handleEvents()
-
-  handleEvents: =>
+    # Handle events
     boundStage = @stage.bind(@)
     @querySelector('.btn').addEventListener('click', boundStage)
     @addEventListener('dblclick', boundStage)


Fix dashboard table sort

Fix dashboard table sort

--- SingularityUI/app/views/dashboard.coffee
+++ SingularityUI/app/views/dashboard.coffee
@@ -6,5 +6,6 @@

     render: =>
         @$el.html @template state: app.state.toJSON()
+        utils.setupSortableTables()

 module.exports = DashboardView


docs; remove uneeded implementation

docs; remove uneeded implementation

--- src/util/spec.coffee
+++ src/util/spec.coffee
@@ -7,38 +7,52 @@

 ###
-  Mixin factory for tests. Note that anything spied on should be added
-  after the Mixin.from_obj, since they need to be modified by jasmine.
+  Mixin factory for tests. Instances aren't frozen, so that jasmine can
+  spy on methods and properties.
+
+  A bit implementation-aware; it knows how to add a spyable method (see
+  @_make_spyable_method) that's not a pre/post-mixing hook.
 ###
 MIXINS =

   FREEZE: false

-  _make_spieable_method: ({mixin, methodname}) ->
+  _make_spyable_method: ({mixin, methodname}) ->
     mixin.mixin_keys.push methodname
     mixin[methodname] = ->
     mixin

+  ###
+    Returns a Mixin with a premixing hook that validates the
+    existence of certain keys on the prototype, specified by schema.
+
+    .premixing_hook is spyable.
+  ###
   schematized_protomixin: (schema = ['special_key']) ->
     mixin = Mixin.from_obj
       name: 'Schematized Example Mixin'
       foo: 'foo'
       , @FREEZE
     mixin.premixing_hook = ->
-      for key in schema || ['special_key';]
+      for key in schema
         unless @[key]?
           throw new errors.NotImplemented "Wanted schema key #{key}"
     mixin

+  ###
+    Returns a Mixin with a premixing hook that validates the
+    existence of certain keys on the prototype, specified by schema.
+
+    .premixing_hook is spyable.
+  ###
   default_protomixin: ->
     mixin = Mixin.from_obj
       name: 'Default Example Mixin'
       foo: 'foo'
       bar: 1
       , @FREEZE
-    mixin = @_make_spieable_method({mixin, methodname: 'baz'})
+    mixin = @_make_spyable_method({mixin, methodname: 'baz'})
     mixin.postmixing_hook = ->
-      @modified_proto = true
     mixin

 module.exports = {beforeOnce, _, MIXINS}


setup test

setup test

--- src/god/test_world.coffee
+++ src/god/test_world.coffee
@@ -151,8 +151,12 @@
   test 'world removes child at index', (t) ->
     a = world.add_child('a')
     b = world.add_child('b')
+    world.map_children (child) ->
+      "just to set index"
     count = world._child_count()
     t.ok count
+    t.ok a.index
+    t.ok b.index
     t.end()

   test 'world has authorities', (t) ->


should be ?= instead?

should be ?= instead?

--- resources/assets/coffee/_classes/beatmapset-filter.coffee
+++ resources/assets/coffee/_classes/beatmapset-filter.coffee
@@ -44,7 +44,7 @@

   @keyToChar: ->
-    @_keyToChar ||= _.invert @charToKey
+    @_keyToChar ?= _.invert @charToKey

   @defaults:


Added: Atom Sass/LESS languages (just in case...)

--- dotfiles/atom/config.cson
+++ dotfiles/atom/config.cson
@@ -13,7 +13,6 @@
       "language-mustache"
       "welcome"
       "language-toml"
-      "language-sass"
       "language-ruby-on-rails"
       "language-ruby"
       "language-python"
@@ -25,7 +24,6 @@
       "language-csharp"
       "language-clojure"
       "bookmarks"
-      "language-less"
       "language-sql"
       "styleguide"
     ]


fix spec for automaticallyEscapeInsertModeOnActivePaneItemChange change

--- spec/vim-state-spec.coffee
+++ spec/vim-state-spec.coffee
@@ -267,12 +267,22 @@
       describe "automaticallyEscapeInsertModeOnActivePaneItemChange = true", ->
         beforeEach ->
           settings.set('automaticallyEscapeInsertModeOnActivePaneItemChange', true)
-
-        it "return to escape mode for all vimEditors", ->
-          pane.activateItem(otherEditor)
-          expect(pane.getActiveItem()).toBe(otherEditor)
-          ensure mode: 'normal'
-          otherVim.ensure mode: 'normal'
+          jasmine.useRealClock()
+
+        it "automatically shift to normal mode except new active editor", ->
+          called = false
+
+          runs ->
+            atom.workspace.onDidStopChangingActivePaneItem -> called = true
+            pane.activateItem(otherEditor)
+
+          waitsFor ->
+            called
+
+          runs ->
+            expect(pane.getActiveItem()).toBe(otherEditor)
+            ensure mode: 'normal'
+            otherVim.ensure mode: 'insert'

   describe "replace-mode", ->
     describe "with content", ->


refactored model+doc

refactored model+doc

--- app/assets/javascripts/model.coffee
+++ app/assets/javascripts/model.coffee
@@ -1,84 +1,68 @@
+# Initializing `Model`
 Model ?= {}

-Model.BinaryStore =
-
-	data : []
-	size : [0,0,0]
-	offset : [0,0,0]
-
-	BUCKET_WIDTH : 64
-
-	bucketIndex : (x, y, z) ->
-		(x / @BUCKET_WIDTH - @offset[0] >> 0) +
-		(y / @BUCKET_WIDTH - @offset[1] >> 0) * @size[0] +
-		(z / @BUCKET_WIDTH - @offset[2] >> 0) * @size[0] * @size[1]
-
-	pointIndex : (x, y, z) ->
-		(x % @BUCKET_WIDTH >> 0) +
-		(y % @BUCKET_WIDTH >> 0) * @BUCKET_WIDTH +
-		(z % @BUCKET_WIDTH >> 0) * @BUCKET_WIDTH * @BUCKET_WIDTH
-
-	extendPoints : (x0, y0, z0, x1, y1, z1) ->
-
-		@extendCube(
-			x0 / @BUCKET_WIDTH >> 0,
-			y0 / @BUCKET_WIDTH >> 0,
-			x0 / @BUCKET_WIDTH >> 0,
-			x1 / @BUCKET_WIDTH >> 0,
-			y1 / @BUCKET_WIDTH >> 0,
-			z1 / @BUCKET_WIDTH >> 0
-		)
-
-	extendCube : (x0, y0, z0, x1, y1, z1) ->
-
-		cubeOffset = [
-			Math.min(x0, x1, @offset[0])
-			Math.min(y0, y1, @offset[1])
-			Math.min(z0, z1, @offset[2])
-		]
-		cubeSize = [
-			Math.max(x0, x1, @offset[0] + @size[0]) - cubeOffset[0] + 1
-			Math.max(y0, y1, @offset[1] + @size[1]) - cubeOffset[1] + 1
-			Math.max(z0, z1, @offset[2] + @size[2]) - cubeOffset[2] + 1
-		]
-
-		cube = []
-		for z in [0...cubeSize[2]]
-			for y in [0...cubeSize[1]]
-				for x in [0...cubeSize[0]]
-					index =
-						(x - @offset[0]) +
-						(y - @offset[1]) * @size[0] +
-						(z - @offset[2]) * @size[0] * @size[1]
-
-					cube.push @data[index]
-
-
-		@data = cube
-		@offset = cubeOffset
-		@size = cubeSize
-
-	value : (x, y, z, newValue) ->
-
-		bucketIndex = @bucketIndex(x, y, z)
-		bucket = @data[bucketIndex]
-
-		unless newValue?
-			if bucket?
-				bucket[@pointIndex(x, y, z)]
-			else
-				-1
-		else
-			if 0 <= bucketIndex < @data.length
-				unless bucket?
-					bucket = @data[bucketIndex] = new Float32Array(@BUCKET_WIDTH * @BUCKET_WIDTH * @BUCKET_WIDTH)
-					bucket[i] = -1 for i in [0...bucket.length]
-				bucket[@pointIndex(x, y, z)] = newValue
-			else
-				throw "cube fault"
-
+# #Model.Binary#
+# Binary is the real deal.
+# It loads and stores the primary graphical data.
+#
+# **Mixins**: Model.LazyInitializable, Model.Synchronizable
+#
+# ##Data structure##
+#
+# ###Concept###
+# We store 3-dimensional data with each coordinate >= [0,0,0].
+# Each point is stored in **buckets** which resemble a cubical grid.
+# Those buckets are kept in an expandable data structure (**cube**) which
+# represents the smallest cuboid covering all used buckets.
+#
+# ###Implementation###
+# Each point value (greyscale color) is represented by a number
+# between 1 and 2, where 1 is black and 2 white. Actually WebGL
+# generally uses [0, 1], but as TypedArrays are initialized with
+# 0 we shift the actual interval to use 0 as an undefined value.
+#
+# The buckets are implemented as `Float32Array`s with a length of
+# `BUCKET_WIDTH ^ 3`. Each point can be easiliy found through simple
+# arithmetik (see `Model.Binary.pointIndex`).
+#
+# The cube is defined by the offset `[x,y,z]` and size `[a,b,c]`
+# of the cuboid. It is actually just a standard javascript array
+# with each item being either `null` or a bucket. The length of the
+# array is `a * b * c`. Also finding th containing bucket of a point
+# can be done with pretty simple math (see `Model.Binary.bucketIndex`).
+#
+# ###Inserting###
+# When inserting new data into the data structure we first need
+# to make sure the cube is big enough to cover all points. Otherwise
+# we'll have to expand the cube (see `Model.Binary.expandCube`).
+# Then we add each point. If the corresponding bucket of a point
+# isn't initialized we'll handle that on the fly
+# (see `Model.Binary.value`).
+#
+#
+# ##Loading##
+# The server provides the coordinates (vertices) and color values of
+# the data separately.
+#
+#
+#
+#         +--+--+--+--+--+--+--+
+#        /  /  /  /  /  /  /  /|
+#       /--/--/--/--/--/--/--/ |
+#      /  /  /  /  /  /  /  /|/|
+#     +--+--+--+--+--+--+--+ | |
+#     |  |  |  |  |  |  |  |/|/|
+#     |--|--|--|--|--|--|--| | |
+#     |  |  |  |  |  |  |  |/|/
+#     |--|--|--|--|--|--|--| /
+#     |  |  |  |  |  |  |  |/
+#     +--+--+--+--+--+--+--+
+#
+#
+
+#
 Model.Binary =
-
+
 	vertexTemplate : null

 	initialize : (callback) ->
@@ -129,6 +113,8 @@

 	get : (vertices, callback) ->

+		_value = @value
+
 		colors = new Float32Array(~~(vertices.length / 3))

 		for i in [0...vertices.length] by 3
@@ -144,61 +130,67 @@
 			colors[i / 3] = if xd == 0
 				if yd == 0
 					if zd == 0
-						@value(x, y, z)
+						_value(x, y, z)
 					else
 						#linear z
-						Interpolation.linear(@value(x, y, z0), @value(x, y, z1), zd)
+						Interpolation.linear(_value(x, y, z0), _value(x, y, z1), zd)
 				else
 					if zd == 0
 						#linear y
-						Interpolation.linear(@value(x, y0, z), @value(x, y1, z), yd)
+						Interpolation.linear(_value(x, y0, z), _value(x, y1, z), yd)
 					else
 						#bilinear y,z
 						Interpolation.bilinear(
-							@value(x, y0, z0),
-							@value(x, y1, z0),
-							@value(x, y0, z1),
-							@value(x, y1, z1),
+							_value(x, y0, z0),
+							_value(x, y1, z0),
+							_value(x, y0, z1),
+							_value(x, y1, z1),
 							yd, zd)
 			else
 				if yd == 0
 					if zd == 0
 						#linear x
-						Interpolation.linear(@value(x0, y, z), @value(x1, y, z), xd)
+						Interpolation.linear(_value(x0, y, z), _value(x1, y, z), xd)
 					else
 						#bilinear x,z
 						Interpolation.bilinear(
-							@value(x0, y, z0),
-							@value(x1, y, z0),
-							@value(x0, y, z1),
-							@value(x1, y, z1),
+							_value(x0, y, z0),
+							_value(x1, y, z0),
+							_value(x0, y, z1),
+							_value(x1, y, z1),
 							xd, zd)
 				else
 					if zd == 0
 						#bilinear x,y
 						Interpolation.bilinear(
-							@value(x0, y0, z),
-							@value(x1, y0, z),
-							@value(x0, y1, z),
-							@value(x1, y1, z),
+							_value(x0, y0, z),
+							_value(x1, y0, z),
+							_value(x0, y1, z),
+							_value(x1, y1, z),
 							xd, yd)
 					else
 						#trilinear x,y,z
 						Interpolation.trilinear(
-							@value(x0, y0, z0),
-							@value(x1, y0, z0),
-							@value(x0, y1, z0),
-							@value(x1, y1, z0),
-							@value(x0, y0, z1),
-							@value(x1, y0, z1),
-							@value(x0, y1, z1),
-							@value(x1, y1, z1),
+							_value(x0, y0, z0),
+							_value(x1, y0, z0),
+							_value(x0, y1, z0),
+							_value(x1, y1, z0),
+							_value(x0, y0, z1),
+							_value(x1, y0, z1),
+							_value(x0, y1, z1),
+							_value(x1, y1, z1),
 							xd, yd, zd
 						)

 		callback(null, colors)

-	getOld : (position, direction, callback) ->
+
+	ping : (position, direction, callback) ->
+
+		@ping = _.throttle2 @_ping, 20000
+		@ping(position, direction, callback)
+
+	_ping : (position, direction, callback) ->

 		@lazyInitialize (err) =>
 			return callback err if err
@@ -224,11 +216,22 @@
 						min_y = if y < min_y then y else min_y
 						min_z = if z < min_z then z else min_z

+					min_x = if x < 0 then 0 else x
+					min_y = if y < 0 then 0 else y
+					min_z = if z < 0 then 0 else z
+					max_x = if x < 0 then 0 else x
+					max_y = if y < 0 then 0 else y
+					max_z = if z < 0 then 0 else z
+

 					@extendPoints(min_x, min_y, min_z, max_x, max_y, max_z)

 					for i in [0...colors.length]
-						@value(vertices[i * 3], vertices[i * 3 + 1], vertices[i * 3 + 2], colors[i] / 256)
+						x = vertices[i * 3]
+						y = vertices[i * 3 + 1]
+						z = vertices[i * 3 + 2]
+						if x >= 0 and y >= 0 and z >= 0
+							@value(x, y, z, colors[i] / 256 + 1)

 					console.timeEnd "e"
 					callback null if callback
@@ -238,12 +241,12 @@

 			@load position, direction, @synchronizingCallback(loadedData, finalCallback)

-	load : (point, direction, callback) ->
+	load : (position, direction, callback) ->
 		@lazyInitialize (err) ->
 			return callback(err) if err

 			request
-				url : "/binary/data/cube?px=#{point[0]}&py=#{point[1]}&pz=#{point[2]}&ax=#{direction[0]}&ay=#{direction[1]}&az=#{direction[2]}"
+				url : "/binary/data/cube?px=#{position[0]}&py=#{position[1]}&pz=#{position[2]}&ax=#{direction[0]}&ay=#{direction[1]}&az=#{direction[2]}"
 				responseType : 'arraybuffer'
 				,
 				(err, data) ->
@@ -251,6 +254,95 @@
 						callback err
 					else
 						callback null, new Uint8Array(data)
+
+	data : []
+	size : [0,0,0]
+	offset : [0,0,0]
+
+	BUCKET_WIDTH : 64
+
+	bucketIndex : (x, y, z) ->
+
+		_bucket_width = @BUCKET_WIDTH
+		_offset = @offset
+		_size = @size
+
+		(x / _bucket_width - _offset[0] >> 0) +
+		(y / _bucket_width - _offset[1] >> 0) * _size[0] +
+		(z / _bucket_width - _offset[2] >> 0) * _size[0] * _size[1]
+
+	pointIndex : (x, y, z) ->
+
+		_bucket_width = @BUCKET_WIDTH
+
+		(x % _bucket_width >> 0) +
+		(y % _bucket_width >> 0) * _bucket_width +
+		(z % _bucket_width >> 0) * _bucket_width * _bucket_width
+
+	extendPoints : (x0, y0, z0, x1, y1, z1) ->
+
+		_bucket_width = @BUCKET_WIDTH
+
+		@extendCube(
+			x0 / _bucket_width >> 0,
+			y0 / _bucket_width >> 0,
+			x0 / _bucket_width >> 0,
+			x1 / _bucket_width >> 0,
+			y1 / _bucket_width >> 0,
+			z1 / _bucket_width >> 0
+		)
+
+	extendCube : (x0, y0, z0, x1, y1, z1) ->
+
+		_offset = @offset
+		_size = @size
+		_data = @data
+
+		cubeOffset = [
+			Math.min(x0, x1, _offset[0])
+			Math.min(y0, y1, _offset[1])
+			Math.min(z0, z1, _offset[2])
+		]
+		cubeSize = [
+			Math.max(x0, x1, _offset[0] + _size[0]) - cubeOffset[0] + 1
+			Math.max(y0, y1, _offset[1] + _size[1]) - cubeOffset[1] + 1
+			Math.max(z0, z1, _offset[2] + _size[2]) - cubeOffset[2] + 1
+		]
+
+		cube = []
+		for z in [0...cubeSize[2]]
+			for y in [0...cubeSize[1]]
+				for x in [0...cubeSize[0]]
+					index =
+						(x - _offset[0]) +
+						(y - _offset[1]) * _size[0] +
+						(z - _offset[2]) * _size[0] * _size[1]
+
+					cube.push _data[index]
+
+
+		@data = cube
+		@offset = cubeOffset
+		@size = cubeSize
+
+	value : (x, y, z, newValue) ->
+
+		bucketIndex = @bucketIndex(x, y, z)
+		bucket = @data[bucketIndex]
+
+		unless newValue?
+			if bucket?
+				bucket[@pointIndex(x, y, z)]
+			else
+				-1
+		else
+			if 0 <= bucketIndex < @data.length
+				unless bucket?
+					_bucket_width = @BUCKET_WIDTH
+					bucket = @data[bucketIndex] = new Float32Array(_bucket_width * _bucket_width * _bucket_width)
+				bucket[@pointIndex(x, y, z)] = newValue
+			else
+				throw "cube fault"

 Model.Mesh =
@@ -317,7 +409,7 @@
 			request url : "/assets/shader/#{name}.vs", (@synchronizingCallback loadedData, (@cachingCallback name, callback))
 			request url : "/assets/shader/#{name}.fs", (@synchronizingCallback loadedData, (@cachingCallback name, callback))

-
+
 Model.Route =

 	dirtyBuffer : []
@@ -390,6 +482,12 @@
 			@dirtyBuffer.push position
 			@push()

+
+
+#################################################
+# Mixins
+#################################################
+
 Model.LazyInitializable =

 	initialized : false
@@ -457,7 +555,10 @@
 			return false

-_.extend Model.Binary, Model.BinaryStore
+#################################################
+# Mixin hook up
+#################################################
+
 _.extend Model.Binary, Model.Synchronizable
 _.extend Model.Binary, Model.LazyInitializable
 _.extend Model.Mesh, Model.Cacheable


Prevent map search return from submitting form

Refs #615

--- app/assets/javascripts/ui.js.coffee
+++ app/assets/javascripts/ui.js.coffee
@@ -1,6 +1,10 @@
 jQuery ->
   # Tabs
   $(".tabs").parent().tabs()
+
+  $(document).on("keypress", "input.search-input", (event) ->
+    event.preventDefault() if (event.keyCode == 13) # do not submit from when searching
+  )

   if history.pushState
     $(".tabs").parent().on "tabsactivate", (event, ui) ->


shortened some var names related to breakpoints

--- client/editor.coffee
+++ client/editor.coffee
@@ -52,8 +52,8 @@
     @$e = $(e).html EDITOR_HTML
     {@id, @name, @emit} = @opts = opts; @isTracer = opts.tracer
     @xline = null # the line number of the empty line inserted when cursor is at eof and you press <down>
-    @breakpoints = [] # array of line numbers
-    @originalText = @originalBreakpoints = '' # remember them to avoid pointless saving on EP; originalBreakpoints is comma-separated line numbers
+    @bp = [] # breakpoints, an array of line numbers
+    @otext = @obp = '' # remember original text and bp to avoid pointless saving on EP; obp is comma-separated line numbers
     @hll = null # highlighted line -- currently executed line in tracer
     @lastQuery = @lastIC = @overlay = @annotation = null # search-related state
     @focusTimestamp = 0
@@ -181,7 +181,7 @@
   updateSize: -> @cm.setSize @$e.width(), @$e.parent().height() - @$e.position().top - 28; return

   open: (ee) ->
-    @cm.setValue @originalText = ee.text; @cm.clearHistory(); @cm.focus()
+    @cm.setValue @otext = ee.text; @cm.clearHistory(); @cm.focus()
     # Constants for entityType:
     # DefinedFunction     1
     # SimpleCharArray     2
@@ -200,9 +200,9 @@
     @cm.setOption 'readOnly', ee.readOnly || ee.debugger
     line = ee.currentRow; col = ee.currentColumn || 0; if line == col == 0 && ee.text.indexOf('\n') < 0 then col = ee.text.length
     @cm.setCursor line, col; @cm.scrollIntoView null, @$e.height() / 2
-    @breakpoints = ee.lineAttributes.stop[..]
-    for l in @breakpoints then @cm.setGutterMarker l, 'breakpoints', @createBreakpointElement()
-    @originalBreakpoints = @breakpoints.join()
+    @bp = ee.lineAttributes.stop[..]
+    for l in @bp then @cm.setGutterMarker l, 'breakpoints', @createBreakpointElement()
+    @obp = @bp.join()
     if D.floating then $('title', @$e[0].ownerDocument).text ee.name
     return
   hasFocus: -> window.focused && @cm.hasFocus()
@@ -231,9 +231,9 @@
     $r.focus().select(); @highlightSearch(); return
   EP: ->
     v = @cm.getValue()
-    if v != @originalText || @breakpoints.join() != @originalBreakpoints
-      for l in @breakpoints then @cm.setGutterMarker l, 'breakpoints', null
-      @emit 'SaveChanges', win: @id, text: @cm.getValue(), attributes: stop: @breakpoints[..].sort (x, y) -> x - y
+    if v != @otext || @bp.join() != @obp
+      for l in @bp then @cm.setGutterMarker l, 'breakpoints', null
+      @emit 'SaveChanges', win: @id, text: @cm.getValue(), attributes: stop: @bp[..].sort (x, y) -> x - y
     else
       @emit 'CloseWindow', win: @id
     return
@@ -307,10 +307,10 @@
     for sel in @cm.listSelections()
       l0 = sel.anchor.line; l1 = sel.head.line; if l0 > l1 then tmp = l0; l0 = l1; l1 = tmp
       for l in [l0..l1] by 1
-        if (i = @breakpoints.indexOf l) >= 0 then @breakpoints.splice i, 1; @cm.setGutterMarker l, 'breakpoints', null
-        else @breakpoints.push l; @cm.setGutterMarker l, 'breakpoints', @createBreakpointElement()
+        if (i = @bp.indexOf l) >= 0 then @bp.splice i, 1; @cm.setGutterMarker l, 'breakpoints', null
+        else @bp.push l; @cm.setGutterMarker l, 'breakpoints', @createBreakpointElement()
     if @isTracer
-      @emit 'SetLineAttributes', win: @id, nLines: @cm.lineCount(), lineAttributes: stop: @breakpoints[..].sort (x, y) -> x - y
+      @emit 'SetLineAttributes', win: @id, nLines: @cm.lineCount(), lineAttributes: stop: @bp[..].sort (x, y) -> x - y
     return
   tabOrAutocomplete: ->
     if @cm.somethingSelected()
@@ -327,7 +327,7 @@
   onbeforeunload: -> # called when the user presses [X] on the OS window
     if @ide.dead
       D.forceCloseNWWindow?(); return
-    else if @cm.getValue() == @originalText && @breakpoints.join() == @originalBreakpoints
+    else if @cm.getValue() == @otext && @bp.join() == @obp
       @EP(); return
     else if !@dialog
       window.focus()


Bug fix on file completion during import

--- client/import.coffee
+++ client/import.coffee
@@ -455,7 +455,8 @@
           file.graphic = graphic
           figure.files.push file
           file.file2id = await new Promise (resolve, reject) ->
-            file.callback = (file2) ->
+            file.callback = (file2, done) ->
+              done() # assume user won't navigate away until import complete
               resolve file2.uniqueIdentifier
             Files.resumable.addFile file
       for figure in attach


add http://

add http://

--- src/coffee/node/api.coffee
+++ src/coffee/node/api.coffee
@@ -12,4 +12,4 @@
     # TODO this is duplicated
     puzzle = PuzzleUtils.getEmptyPuzzle 15, 15, req.body.title
     Db.createPuzzle puzzle, (puzzleID) ->
-        callback {success: true, url: req.headers.host + "/puzzle/#{puzzleID}"}
+        callback {success: true, url: "http://" + req.headers.host + "/puzzle/#{puzzleID}"}


Add "initialized" notification

Add "initialized" notification

--- lib/assets/javascripts/cable/subscriber_manager.js.coffee
+++ lib/assets/javascripts/cable/subscriber_manager.js.coffee
@@ -5,6 +5,7 @@
   add: (subscriber) ->
     {identifier} = subscriber
     @subscribers[identifier] = subscriber
+    @notify(subscriber, "initialized")
     if @sendCommand("subscribe", identifier)
       @notify(subscriber, "connected")


Fixed a bad method name

Fixed a bad method name

--- app/coffee/controllers/tasks.coffee
+++ app/coffee/controllers/tasks.coffee
@@ -41,7 +41,7 @@

     # Load initial data
     $data.loadProject($scope)
-    $data.loadUserAndRoles($scope)
+    $data.loadUsersAndRoles($scope)

     # Initial load
     loadAttachments()


Rename for mocks to get around cache finding wrong model type

--- test/lib/build_mocks.coffee
+++ test/lib/build_mocks.coffee
@@ -11,19 +11,20 @@
   schema = _.clone(compiled_schema.raw)
   for key, relation of compiled_schema.relations
     do (key, relation) ->
-      schema[key] = ->
+      schema["mock_#{key}"] = ->
         field = compiled_schema.raw[key]
         field = field() if _.isFunction(field)
         field = _.clone(field)
         relation = model_type.relation(key)
         if relation.join_table
-          join_table_info = {join_table: createMockModel(mock_model_types, relation.join_table)} # override existing join table
-          mock_model_types[join_table_info.join_table.model_name] = join_table_info.join_table
+          mock_join_table = createMockModel(mock_model_types, relation.join_table)
+          mock_model_types[mock_join_table.model_name] = mock_join_table
+          join_table_info = {join_table: createMockModel(mock_model_types, relation.join_table)}
           if field.length is 2 then field.push(join_table_info) else _.extend(field[2], join_table_info)
         return field

   class MockModel extends Backbone.Model
-    @model_name: model_type.model_name
+    @model_name: "Mock#{model_type.model_name}"
     @schema: schema
     sync: require('backbone-orm/memory_sync')(MockModel)


test: make test clone obj failed work on the node v12

--- test/properties-test.coffee
+++ test/properties-test.coffee
@@ -134,7 +134,10 @@
     result.isDefaultObject(obj).should.be.false

   it 'should throw a hint error when clone object failed', ->
-    props = Properties obj: value:console
+    class DontClone
+      clone: ->
+        throw new Error('disable clone')
+    props = Properties obj: value:new DontClone()
     result = {}
     expect(props.initializeTo.bind(props, result)).to.be.throw 'the attribute "obj" can not be cloned, set descriptor "clone" to false'


added ⎕off

added ⎕off

--- src/vocabulary/quad.coffee
+++ src/vocabulary/quad.coffee
@@ -66,3 +66,5 @@
       if isInt x, 0, 0x10000 then y = String.fromCharCode x
       else if typeof x is 'string' then y = x.charCodeAt 0
       else domainError()
+
+  'get_⎕OFF': -> if process? then process.exit 0 else nonceError()


Remove cruft

Remove cruft

--- test/spec/range_spec.coffee
+++ test/spec/range_spec.coffee
@@ -42,7 +42,6 @@
     mockSelection = (ii) -> new h.MockSelection(h.fix(), testData[ii])

   afterEach ->
-    delete a
     h.clearFixtures()

   describe "SerializedRange", ->


adds nesting depth limit to js driver

adds nesting depth limit to js driver

--- drivers/javascript/src/query.coffee
+++ drivers/javascript/src/query.coffee
@@ -3,18 +3,25 @@
 goog.require("rethinkdb.base")
 goog.require("rethinkdb.ast")

-rethinkdb.expr = ar (val) ->
+rethinkdb.expr = varar 1, 2, (val, nestingDepth=20) ->
     if val is undefined
         throw new RqlDriverError "Cannot wrap undefined with r.expr()."
+
+    if nestingDepth <= 0
+        throw new RqlDriverError "Nesting depth limit exceeded"

     else if val instanceof TermBase
         val
     else if val instanceof Function
         new Func {}, val
     else if goog.isArray val
+        val = (rethinkdb.expr(v, nestingDepth - 1) for v in val)
         new MakeArray {}, val...
     else if goog.isObject val
-        new MakeObject val
+        obj = {}
+        for own k,v of val
+            obj[k] = rethinkdb.expr(v, nestingDepth - 1)
+        new MakeObject obj
     else
         new DatumTerm val

@@ -72,8 +79,11 @@

 # Use r.json to serialize as much of the obect as JSON as is
 # feasible to avoid doing too much protobuf serialization.
-rethinkdb.exprJSON = ar (val) ->
-    if isJSON(val)
+rethinkdb.exprJSON = varar 1, 2, (val, nestingDepth=20) ->
+    if nestingDepth <= 0
+        throw new RqlDriverError "Nesting depth limit exceeded"
+
+    if isJSON(val, nestingDepth - 1)
         rethinkdb.json(JSON.stringify(val))
     else if (val instanceof TermBase)
         val
@@ -84,17 +94,20 @@
             wrapped = {}

         for k,v of val
-            wrapped[k] = rethinkdb.exprJSON(v)
+            wrapped[k] = rethinkdb.exprJSON(v, nestingDepth - 1)
         rethinkdb.expr(wrapped)

 # Is this JS value representable as JSON?
-isJSON = (val) ->
+isJSON = (val, nestingDepth=20) ->
+    if nestingDepth <= 0
+        throw new RqlDriverError "Nesting depth limit exceeded"
+
     if (val instanceof TermBase)
         false
     else if (val instanceof Object)
         # Covers array case as well
         for k,v of val
-            if not isJSON(v) then return false
+            if not isJSON(v, nestingDepth - 1) then return false
         true
     else
         # Primitive types can always be represented as JSON


#11 удаление созданных директорий

#11 удаление созданных директорий

--- test/test-copy-files.coffee
+++ test/test-copy-files.coffee
@@ -11,8 +11,10 @@
 ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789'.split('')
 SRC_DIR  = '/tmp/test-copy-files/src'
 DST_DIR  = '/tmp/test-copy-files/dst'
-NUM_OF_FILES = 5
+NUM_OF_FILES = 20
 FILE_SIZE_LIMIT = 1000
+
+g_createdDirs = []

 rand = (n) -> Math.floor(Math.random()*n)

@@ -22,11 +24,12 @@
     name += alphabet[rand(alphabet.length)]
   return name

-mkdirp = (path, mode = 0755) ->
-  return
+mkdirp = (path, mode = 0755) ->
   parent = dirname(path)
   mkdirp(parent, mode) unless existsSync(parent)
-  fs.mkdirSync(path, mode) unless existsSync(path)
+  unless existsSync(path)
+    fs.mkdirSync(path, mode)
+    g_createdDirs.push(path)

 cleanDir = (path) ->
   for f in fs.readdirSync(path)
@@ -67,4 +70,12 @@
           srcStat = fs.lstatSync(SRC_DIR)
           dstStat = fs.lstatSync(DST_DIR)
           assert.equal srcStat.size, dstStat.size
+}).addBatch({
+  'clean':
+    topic: ->
+      cleanDir(SRC_DIR)
+      cleanDir(DST_DIR)
+      while g_createdDirs.length > 0
+        fs.rmdirSync(g_createdDirs.pop())
+    'cleaned': ->
 }).export(module)


遷移元の都道府県一覧ではなく、遷移してから開栓情報を取得するように変更

遷移元の都道府県一覧ではなく、遷移してから開栓情報を取得するように変更

--- app/controllers/shopAreaData.coffee
+++ app/controllers/shopAreaData.coffee
@@ -13,22 +13,24 @@

 $.shopArea.addEventListener 'click',(e) ->
-  $.activityIndicator.show()
   placeData = e.row.placeData
-  Cloud.Statuses.query
-      page: 1
-      per_page: 20
-      where:
-        place_id:placeData.placeID
-    , (e) ->
-      $.activityIndicator.hide()
-      if e.success
-        placeData.statuses = e.statuses
-      else
-        placeData.statuses = []
+  shopDataDetailController = Alloy.createController('shopDataDetail')
+  shopDataDetailController.move($.tabOne,placeData)
+
+  # Cloud.Statuses.query
+  #     page: 1
+  #     per_page: 20
+  #     where:
+  #       place_id:placeData.placeID
+  #   , (e) ->
+
+  #     if e.success
+  #       placeData.statuses = e.statuses
+  #     else
+  #       placeData.statuses = []

-      shopDataDetailController = Alloy.createController('shopDataDetail')
-      shopDataDetailController.move($.tabOne,placeData)
+  #     shopDataDetailController = Alloy.createController('shopDataDetail')
+  #     shopDataDetailController.move($.tabOne,placeData)

 createShopDataRow = (placeData) ->


Added test for inner an shadowType in constructor

--- test/tests/LayerTest.coffee
+++ test/tests/LayerTest.coffee
@@ -956,6 +956,15 @@
 				x: 10
 			l.shadow1.type.should.equal "inner"
 			l.style.boxShadow.should.equal "rgba(123, 123, 123, 0.498039) 10px 0px 0px 0px inset"
+
+		it "should use inner as an alias for inset shadow in the constructor", ->
+			l = new Layer
+				shadowType: "inner"
+				shadowColor: "red"
+				shadowY: 10
+				shadowBlur: 10
+			l.shadow1.type.should.equal "inner"
+			l.style.boxShadow.should.equal "rgb(255, 0, 0) 0px 10px 10px 0px inset"

 	describe "Events", ->


Fix for the master checkbox.

Fix for the master checkbox.

--- app/scripts/controllers/main.coffee
+++ app/scripts/controllers/main.coffee
@@ -31,6 +31,9 @@
       # Whether no VMs are checked.
       $scope.none = true

+      $scope.$watch 'checked_master', (checked_master) ->
+        $scope.all = checked_master
+        $scope.none = !checked_master
       $scope.$watchCollection 'checked_VMs', (checked_VMs) ->
         all = none = true


Make donut more async-friendly (fix #67)

--- app/scripts/directives/donut.coffee
+++ app/scripts/directives/donut.coffee
@@ -1,7 +1,7 @@
 'use strict'

 angular.module('holmesApp')
-  .directive('donut', ($rootScope) ->
+  .directive 'donut', ($rootScope) ->
     restrict: 'E'
     template: '

'
     replace: true
@@ -56,17 +56,17 @@
             labelElement.stop(true, true).html(label).fadeIn(200)
         )

-      setData = ->
+      setData = (val) ->
+        return if typeof val == 'undefined'
         element.css('width', width).css('height', height)
-        donut = Morris.Donut(
+        donut = Morris.Donut
           element: chartElement
-          data: data
+          data: val
           colors: colors
           formatter: (value, data) ->
             executeOnSelect(value, data)
             setLabel(data.label)
             return Morris.commas(value, data)
-        )

         deselect = ->
           for segment in donut.segments
@@ -78,7 +78,6 @@
         element.bind('mouseleave', deselect)
         deselect()

-      attrs.$observe('data', setData); # lets just observe only the data because it is
-                                       # bad to use many observers, anyway this won't work
-                                       # without supplied data
-  )
+      scope.$watch 'data', setData # lets just observe only the data because it is
+                                   # bad to use many observers, anyway this won't work
+                                   # without supplied data


Make all edit session uri's relative

Make all edit session uri's relative

This allows them to be collaborated without having absolute paths
in the shared document.

--- lib/image-edit-session.coffee
+++ lib/image-edit-session.coffee
@@ -18,7 +18,8 @@
         new ImageEditSession(filePath)

   @deserialize: ({path}={}) ->
-    if fsUtils.exists(path)
+    path = project.resolve(path)
+    if fsUtils.isFileSync(path)
       new ImageEditSession(path)
     else
       console.warn "Could not build image edit session for path '#{path}' because that file no longer exists"
@@ -27,7 +28,7 @@

   serialize: ->
     deserializer: 'ImageEditSession'
-    path: @path
+    path: @getUri()

   getViewClass: ->
     require './image-view'
@@ -48,7 +49,7 @@
   # Retrieves the URI of the current image.
   #
   # Returns a {String}.
-  getUri: -> @path
+  getUri: -> project?.relativize(@getPath()) ? @getPath()

   # Retrieves the path of the current image.
   #


fix a number of kernel references

fix a number of kernel references

--- library/membrane/mongo.coffee
+++ library/membrane/mongo.coffee
@@ -101,10 +101,10 @@
         assert.ok _.isObject @kernel.mongo = client
         scope = _.isString database = config.database
         message = "Setting the MongoDB database: %s"
-        kernel.mongo = client.db database if scope
+        @kernel.mongo = client.db database if scope
         logger.info message.magenta, database if scope
-        @emit "mongo-ready", @kernel.redis, kernel
-        kernel.emit "mongo-ready", kernel.redis
+        @emit "mongo-ready", @kernel.redis, @kernel
+        @kernel.emit "mongo-ready", @kernel.redis
         next.call this, undefined; return this

     # A hook that will be called prior to instantiating the service


More coding changes

More coding changes

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -283,11 +283,11 @@
     # in with the located and instantiated services. Please refer
     # to the implementation on how and what is being done exactly.
     setupRoutableServices: ->
-        tag = nconf.get "NODE_ENV"
+        tag = nconf.get "NODE_ENV" or null
         missing = "No NODE_ENV variable found"
-        assert _.isString(tag), missing
+        assert not _.isEmpty(tag), missing
         @scope = scoping.Scope.lookupOrFail tag
         assert @scope.incorporate this
         @router = new routing.Router this
-        @middleware = @router.middleware
+        assert @middleware = @router.middleware
         @middleware = @middleware.bind @router


Change some params ordering

Change some params ordering

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -68,7 +68,7 @@
 # rather than writing and ending the request in a direct way.
 module.exports.sender = (kernel) ->
     (request, response, next) ->
-        response.send = (content, typed, keepalive) ->
+        response.send = (content, keepalive, typed) ->
             @emit "sending", content, typed, keepalive
             @setHeader "Content-Type", typed if typed?
             return @write content.toString() if typed?


small fix

small fix

--- dev/gen.iced
+++ dev/gen.iced
@@ -5,7 +5,7 @@
 main = (cb) ->
   esc = make_esc cb, "main"
   asp = new ASP { }
-  await KeyBundle.generate { asp, nsubs : 1, userid : 'Hello (hi) <themax@gmail.com>' }, esc defer bundle
+  await KeyBundle.generate { asp, nsubs : 1, userid : 'maxtaco' }, esc defer bundle
   await bundle.sign {asp}, esc defer()
   await bundle.export_pgp_public_to_client {asp}, esc defer pub
   console.log pub


finish unit test for incremental update

finish unit test for incremental update

--- services/clsi/test/unit/coffee/ResourceWriterTests.coffee
+++ services/clsi/test/unit/coffee/ResourceWriterTests.coffee
@@ -54,7 +54,6 @@
 					.should.equal true

 		it "should store the sync state", ->
-			console.log "CHECKING", @syncState, @basePath
 			@ResourceWriter.storeSyncState
 				.calledWith(@syncState, @basePath)
 				.should.equal true
@@ -77,7 +76,7 @@
 			@ResourceWriter.checkSyncState = sinon.stub().callsArgWith(2, null, true)
 			@ResourceWriter.storeSyncState = sinon.stub().callsArg(2)
 			@ResourceListManager.saveResourceList = sinon.stub().callsArg(2)
-			@ResourceListManager.loadResourceList = sinon.stub().callsArg(1)
+			@ResourceListManager.loadResourceList = sinon.stub().callsArgWith(1, null, @resources)
 			@ResourceWriter.syncResourcesToDisk({
 				project_id: @project_id,
 				syncType: "incremental",


Update RangesTracker to include undo logic

--- services/document-updater/app/coffee/RangesTracker.coffee
+++ services/document-updater/app/coffee/RangesTracker.coffee
@@ -1,5 +1,5 @@
-load = (EventEmitter) ->
-	class RangesTracker extends EventEmitter
+load = () ->
+	class RangesTracker
 		# The purpose of this class is to track a set of inserts and deletes to a document, like
 		# track changes in Word. We store these as a set of ShareJs style ranges:
 		#   {i: "foo", p: 42} # Insert 'foo' at offset 42
@@ -36,6 +36,7 @@
 		#     middle of a previous insert by the first user, the original insert will be split into two.
 		constructor: (@changes = [], @comments = []) ->
 			@setIdSeed(RangesTracker.generateIdSeed())
+			@resetDirtyState()

 		getIdSeed: () ->
 			return @id_seed
@@ -75,7 +76,7 @@
 			comment = @getComment(comment_id)
 			return if !comment?
 			@comments = @comments.filter (c) -> c.id != comment_id
-			@emit "comment:removed", comment
+			@_markAsDirty comment, "comment", "removed"

 		getChange: (change_id) ->
 			change = null
@@ -103,7 +104,11 @@
 				@addComment(op, metadata)
 			else
 				throw new Error("unknown op type")
-
+
+		applyOps: (ops, metadata = {}) ->
+			for op in ops
+				@applyOp(op, metadata)
+
 		addComment: (op, metadata) ->
 			# TODO: Don't allow overlapping comments?
 			@comments.push comment = {
@@ -114,18 +119,18 @@
 					t: op.t
 				metadata
 			}
-			@emit "comment:added", comment
+			@_markAsDirty comment, "comment", "added"
 			return comment

 		applyInsertToComments: (op) ->
 			for comment in @comments
 				if op.p <= comment.op.p
 					comment.op.p += op.i.length
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"
 				else if op.p < comment.op.p + comment.op.c.length
 					offset = op.p - comment.op.p
 					comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...]
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"

 		applyDeleteToComments: (op) ->
 			op_start = op.p
@@ -138,7 +143,7 @@
 				if op_end <= comment_start
 					# delete is fully before comment
 					comment.op.p -= op_length
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"
 				else if op_start >= comment_end
 					# delete is fully after comment, nothing to do
 				else
@@ -161,12 +166,13 @@

 					comment.op.p = Math.min(comment_start, op_start)
 					comment.op.c = remaining_before + remaining_after
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"

 		applyInsertToChanges: (op, metadata) ->
 			op_start = op.p
 			op_length = op.i.length
 			op_end = op.p + op_length
+			undoing = !!op.u

 			already_merged = false
@@ -184,8 +190,9 @@
 						change.op.p += op_length
 						moved_changes.push change
 					else if op_start == change_start
-						# If the insert matches the start of the delete, just remove it from the delete instead
-						if change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
+						# If we are undoing, then we want to cancel any existing delete ranges if we can.
+						# Check if the insert matches the start of the delete, and just remove it from the delete instead if so.
+						if undoing and change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
 							change.op.d = change.op.d.slice(op.i.length)
 							change.op.p += op.i.length
 							if change.op.d == ""
@@ -203,15 +210,15 @@
 					# Only merge inserts if they are from the same user
 					is_same_user = metadata.user_id == change.metadata.user_id

-					# If this is an insert op at the end of an existing insert with a delete following, and it cancels out the following
-					# delete then we shouldn't append it to this insert, but instead only cancel the following delete.
+					# If we are undoing, then our changes will be removed from any delete ops just after. In that case, if there is also
+					# an insert op just before, then we shouldn't append it to this insert, but instead only cancel the following delete.
 					# E.g.
 					#                   foo|<--- about to insert 'b' here
 					#  inserted 'foo'  --^ ^-- deleted 'bar'
 					# should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
 					next_change = @changes[i+1]
 					is_op_adjacent_to_next_delete = next_change? and next_change.op.d? and op.p == change_end and next_change.op.p == op.p
-					will_op_cancel_next_delete = is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
+					will_op_cancel_next_delete = undoing and is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i

 					# If there is a delete at the start of the insert, and we're inserting
 					# at the start, we SHOULDN'T merge since the delete acts as a partition.
@@ -281,8 +288,8 @@
 			for change in remove_changes
 				@_removeChange change

-			if moved_changes.length > 0
-				@emit "changes:moved", moved_changes
+			for change in moved_changes
+				@_markAsDirty change, "change", "moved"

 		applyDeleteToChanges: (op, metadata) ->
 			op_start = op.p
@@ -406,8 +413,8 @@
 					@_removeChange change
 					moved_changes = moved_changes.filter (c) -> c != change

-			if moved_changes.length > 0
-				@emit "changes:moved", moved_changes
+			for change in moved_changes
+				@_markAsDirty change, "change", "moved"

 		_addOp: (op, metadata) ->
 			change = {
@@ -427,17 +434,11 @@
 				else
 					return -1

-			if op.d?
-				@emit "delete:added", change
-			else if op.i?
-				@emit "insert:added", change
+			@_markAsDirty(change, "change", "added")

 		_removeChange: (change) ->
 			@changes = @changes.filter (c) -> c.id != change.id
-			if change.op.d?
-				@emit "delete:removed", change
-			else if change.op.i?
-				@emit "insert:removed", change
+			@_markAsDirty change, "change", "removed"

 		_applyOpModifications: (content, op_modifications) ->
 			# Put in descending position order, with deleting first if at the same offset
@@ -486,13 +487,32 @@
 					previous_change = change
 			return { moved_changes, remove_changes }

+		resetDirtyState: () ->
+			@_dirtyState = {
+				comment: {
+					moved: {}
+					removed: {}
+					added: {}
+				}
+				change: {
+					moved: {}
+					removed: {}
+					added: {}
+				}
+			}
+
+		getDirtyState: () ->
+			return @_dirtyState
+
+		_markAsDirty: (object, type, action) ->
+			@_dirtyState[type][action][object.id] = object
+
 		_clone: (object) ->
 			clone = {}
 			(clone[k] = v for k,v of object)
 			return clone

 if define?
-	define ["utils/EventEmitter"], load
+	define [], load
 else
-	EventEmitter = require("events").EventEmitter
-	module.exports = load(EventEmitter)+	module.exports = load()


removed needless injections

removed needless injections

--- spec/javascripts/unit/controllers/app_ctrl_spec.js.coffee
+++ spec/javascripts/unit/controllers/app_ctrl_spec.js.coffee
@@ -4,7 +4,6 @@
   beforeEach ->
     @initAppCtrl = ->
       @$controller 'AppCtrl',
-        $scope: @$scope
         $location: @$location
         Auth: @Auth