fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -1,238 +1,479 @@
 class Dashing.FullpieAgent extends Dashing.Widget
   #@accessor 'value'
   @accessor 'data'
+
   onData: (data) ->
     $(@node).fadeOut().fadeIn()
-    #@buildPieStructure()
-    #@render(data.data)
-    @update(data.data)
-  buildPieStructure: ->
-    @width = 300
-    @height = 300
-    @radius = Math.min(@width, @height) / 2
-    @color = d3.scale.category20()
-    @pie = d3.layout.pie().sort(null)
-    @arc = d3.svg.arc().innerRadius(@radius - 100).outerRadius(@radius - 50)
-    @svg = d3.select(@node).append('svg').attr('width', @width).attr('height', @height).append('g').attr('transform', 'translate(' + @width / 2 + ',' + @height / 2 + ')')
-    return
-  oldPieData: ''
+    @render(data.data)
+
+  renderX: (data) ->
+    if !data
+      data = @get("value")
+    if !data
+      return
+
+    $(@node).children(".title").text($(@node).attr("data-title"))
+    $(@node).children(".more-info").text($(@node).attr("data-moreinfo"))
+    $(@node).children(".updated-at").text(@get('updatedAtMessage'))
+
+    width = 750 #width
+    height = 450 #height
+    radiuso = 135 #outer radius
+    radiusi = 90 #inner radius
+    labelRadius = 180
+
+    #color = d3.scale.ordinal()
+      #.domain([1,10])
+    #  .domain([0,2])
+      #.range( ['#222222','#555555','#777777','#999999','#bbbbbb','#dddddd','#ffffff'] )
+      #.range( ['#222222','#333333','#444444','#555555','#666666','#777777','#888888','#999999','#aaaaaa'] )
+    #  .range( ['#111111','#222222','#333333','#444444','#555555','#666666','#777777','#888888','#999999','#aaaaaa','#bbbbbb','#cccccc'] )
+    color = d3.scale.category20()
+
+    $(@node).children("svg").remove();
+
+    vis = d3.select(@node).append("svg:svg")
+      .data([data])
+        .attr("width", width)
+        .attr("height", height)
+      .append("svg:g")
+        .attr("transform", "translate(" + width/2 + "," + height/2 + ")")
+
+    arc = d3.svg.arc()
+      .outerRadius(radiuso)
+      .innerRadius(radiusi)
+
+    pie = d3.layout.pie().value((d) -> d.value)
+
+    arcs = vis.selectAll("g.slice")
+      .data(pie)
+      .enter().append("svg:g").attr("class", "slice")
+
+    defs = arcs.append("defs")
+    filter = defs.append("filter")
+        .attr("id","dropshadow")
+    filter.append("feGaussianBlur")
+        .attr("in","SourceAlpha")
+        .attr("stdDeviation","1.1")
+        .attr("result","blur")
+    filter.append("feOffset")
+        .attr("in","blur")
+        .attr("dx",2)
+        .attr("dy",2)
+        .attr("result","offsetBlur")
+    feMerge = filter.append("feMerge")
+    feMerge.append("feMergeNode")
+        .attr("in","offsetBlur")
+    feMerge.append("feMergeNode")
+        .attr("in","SourceGraphic")
+
+    arcs.append("svg:path").attr("fill", (d, i) -> color i)
+      .attr("fill-opacity", 0.7).attr("d", arc)
+      .attr("filter","url(#dropshadow)")
+
+    sum=0
+    for val in data
+      sum += val.value
+
+    if !sum
+      arcs.append("svg:text").attr("class","cusipie-label")
+        .attr('fill', "#fff")
+        .attr("text-anchor", "middle").text("No Results")#.attr('font-size', mFontSize + 'px')
+    else
+      arcs.append("svg:text").attr("class","cusipie-sum")
+        .attr('fill', "#fff")
+        .attr("text-anchor", "middle")
+        .attr("alignment-baseline", "mathematical")
+        .attr('font-size', radiusi*1.3 + 'px')
+        .attr('font-weight', 'bold')
+        .text(sum)
+        .attr("filter","url(#dropshadow)")
+
+      arcs.append("svg:text").attr("transform", (d, i) ->
+        #procent_val = Math.round(data[i].value/sum * 100)
+        #d.innerRadius = (radiuso * (100-procent_val)/100) - radiuso/2 #45  #45=max text size/2
+        #d.innerRadius = 0 #(radiuso * (100-procent_val)/100) - radiuso/2 #45  #45=max text size/2
+        #d.outerRadius = radiuso
+        #"translate(" + arc.centroid(d) + ")")
+
+        c = arc.centroid(d)
+        x = c[0]
+        y = c[1]
+        mx = 1.7
+        my = 1
+        h = Math.sqrt(x*x + y*y)
+        r = labelRadius
+        if x <= 0
+            r = -r
+        if x < 0 and y < 0 and x > -radiusi and x/y < 1.2
+            x = x * mx
+            y = y * my
+        return "translate(" + (x/h * labelRadius) + "," + (y/h * labelRadius) + ")"
+        #return "translate(" + r + "," + y + ")"#(y/h * labelRadius) + ")"
+        #return "translate(" + ((x/h * labelRadius)+r/4) + "," + y*my + ")"#(y/h * labelRadius) + ")"
+        #return "translate(" + (x+r/2)*mx + "," + y*my + ")"
+        #return "translate(" + (x/h * labelRadius)*mx + "," + (y/h * labelRadius)*my + ")"
+        )
+
+        .append("svg:tspan").attr("class","cusipie-label")
+
+        .attr('fill', "#fff")
+        .attr("text-anchor", "middle")
+        .text((d, i) ->
+          if data[i].value != 0
+            #data[i].label
+            data[i].label + " - " + data[i].value
+        )
+        #.attr('font-size', mFontSize + 'px')
+        .attr("filter","url(#dropshadow)")
+        .append('svg:tspan').attr("class","cusipie-label-percent")
+        .attr('x', 0)
+        .attr('dy', '.9em')
+        #.attr('font-size', '70%')
+        .text((d,i) ->
+          if data[i].value != 0
+            #data[i].value + ' (' + Math.round(data[i].value/sum * 100) + '%)'
+            '(' + Math.round(data[i].value/sum * 100) + '%)'
+        )
+
   pieTween: (d, i) ->
-    `var i`
-    that = this
-    #theOldDataInPie = @oldPieData
-    # Interpolate the arcs in data space
-    #s0 = undefined
-    #e0 = undefined
-    #if theOldDataInPie[i]
-    #  s0 = theOldDataInPie[i].startAngle
-    #  e0 = theOldDataInPie[i].endAngle
-    #else if !theOldDataInPie[i] and theOldDataInPie[i - 1]
-    #  s0 = theOldDataInPie[i - 1].endAngle
-    #  e0 = theOldDataInPie[i - 1].endAngle
-    #else if !theOldDataInPie[i - 1] and theOldDataInPie.length > 0
-    #  s0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle
-    #  e0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle
-    #else
-    #  s0 = 0
-    #  e0 = 0
-    # TS
-    s0 = 0
-    e0 = 0
-    #
-    i = d3.interpolate({
-      startAngle: 0
-      endAngle: 0
-    },
-      startAngle: d.startAngle
-      endAngle: d.endAngle)
-    (t) ->
-      b = i(t)
-      return @arc b
-  removePieTween: (d, i) ->
-    `var i`
-    that = this
-    s0 = 2 * Math.PI
-    e0 = 2 * Math.PI
-    i = d3.interpolate({
-      startAngle: d.startAngle
-      endAngle: d.endAngle
-    },
-      startAngle: s0
-      endAngle: e0)
-    (t) ->
-      b = i(t)
-      @arc b
-  update: (dataSet) ->
-    if !dataSet
-        dataSet = @get("data")
-    if !dataSet
-        return
-    width = 750
-    height = 450
-    radius = Math.min(width, height) / 2
-    radiuso = 135
-    radiusi = 90
-    color = d3.scale.category20()
-    pie = d3.layout.pie()#.sort(null)
-    arc = d3.svg.arc().innerRadius(radiusi).outerRadius(radiuso)#.innerRadius(radius - 100).outerRadius(radius - 50)
-    svg = d3.select(@node).append('svg').attr('width', width).attr('height', height).append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
-    console.log 'update pie', dataSet
-    that = this
-    piedata = pie(dataSet[0].value)
-    console.log 'update pie', piedata
-    #create a marker element if it doesn't already exist
-    defs = svg.select('defs')
-    if defs.empty()
-      defs = svg.append('defs')
-    marker = defs.select('marker#circ')
-    if marker.empty()
-      defs.append('marker').attr('id', 'circ').attr('markerWidth', 6).attr('markerHeight', 6).attr('refX', 3).attr('refY', 3).append('circle').attr('cx', 3).attr('cy', 3).attr 'r', 3
-    #Create/select <g> elements to hold the different types of graphics
-    #and keep them in the correct drawing order
-    pathGroup = svg.select('g.piePaths')
-    if pathGroup.empty()
-      pathGroup = svg.append('g').attr('class', 'piePaths')
-    pointerGroup = svg.select('g.pointers')
-    if pointerGroup.empty()
-      pointerGroup = svg.append('g').attr('class', 'pointers')
-    labelGroup = svg.select('g.labels')
-    if labelGroup.empty()
-      labelGroup = svg.append('g').attr('class', 'labels')
-    path = pathGroup.selectAll('path.pie').data(piedata)
-    #console.log("path",this.path.empty())
-    path.enter().append('path').attr('class', 'pie').attr 'fill', (d, i) ->
-      color i
-    #@path.transition().duration(1500).attrTween 'd', that.pieTween
-    path.transition()
-        .duration(1000)
-        #.attrTween("d", pieTween)
-        .attrTween("d", (d,i) ->
-            #console.log("d",d)
-            #console.log("i",i)
-            i = d3.interpolate({startAngle: 0, endAngle: 0}, {startAngle: d.startAngle, endAngle: d.endAngle})
-            return (t) ->
-                b = i(t)
-                return arc(b)
-        )
-    #@path.exit().transition().duration(300).attrTween('d', that.removePieTween).remove()
-    path.exit()
-        .transition()
-        .duration(300)
-        .attrTween("d", (d,i) ->
-            i = d3.interpolate({startAngle: d.startAngle,endAngle: d.endAngle},{startAngle: 2 * Math.PI,endAngle: 2 * Math.PI})
-            return (t) ->
-                b = i(t)
-                return arc(b)
-        )
-        .remove()
-    labels = labelGroup.selectAll('text').data(piedata.sort((p1, p2) ->
-      p1.startAngle - p2.startAngle
-    ))
-    labels.enter().append('text').attr 'text-anchor', 'middle'
-    labels.exit().remove()
-    labelLayout = d3.geom.quadtree().extent([
-      [
-        -that.width
-        -that.height
-      ]
-      [
-        that.width
-        that.height
-      ]
-    ]).x((d) ->
-      d.x
-    ).y((d) ->
-      d.y
-    )([])
-    #create an empty quadtree to hold label positions
-    maxLabelWidth = 0
-    maxLabelHeight = 0
-    labels.text((d) ->
-      # Set the text *first*, so we can query the size
-      # of the label with .getBBox()
-      d.value
-    ).each((d, i) ->
-      # Move all calculations into the each function.
-      # Position values are stored in the data object
-      # so can be accessed later when drawing the line
-      ### calculate the position of the center marker ###
-      a = (d.startAngle + d.endAngle) / 2
-      #trig functions adjusted to use the angle relative
-      #to the "12 o'clock" vector:
-      #console.log 'ts: a', a
-      d.cx = Math.sin(a) * (radius - 75)
-      d.cy = -Math.cos(a) * (radius - 75)
-      ### calculate the default position for the label,
-         so that the middle of the label is centered in the arc
-      ###
-      bbox = @getBBox()
-      #bbox.width and bbox.height will
-      #describe the size of the label text
-      labelRadius = radius - 20
-      d.x = Math.sin(a) * labelRadius
-      d.l = d.x - bbox.width / 2 - 2
-      d.r = d.x + bbox.width / 2 + 2
-      d.y = -Math.cos(a) * (radius - 20)
-      d.b = d.oy = d.y + 5
-      d.t = d.y - bbox.height - 5
-      #console.log 'ts: d', d
-      ### check whether the default position
-         overlaps any other labels
-      ###
-      conflicts = []
-      labelLayout.visit (node, x1, y1, x2, y2) ->
-        #recurse down the tree, adding any overlapping
-        #node is the node in the quadtree,
-        #node.point is the value that we added to the tree
-        #x1,y1,x2,y2 are the bounds of the rectangle that
-        #this node covers
-        if x1 > d.r + maxLabelWidth / 2 or x2 < d.l - maxLabelWidth / 2 or y1 > d.b + maxLabelHeight / 2 or y2 < d.t - maxLabelHeight / 2
-          return true
-        #don't bother visiting children or checking this node
-        p = node.point
-        v = false
-        h = false
-        if p
-          #p is defined, i.e., there is a value stored in this node
-          h = p.l > d.l and p.l <= d.r or p.r > d.l and p.r <= d.r or p.l < d.l and p.r >= d.r
-          #horizontal conflict
-          v = p.t > d.t and p.t <= d.b or p.b > d.t and p.b <= d.b or p.t < d.t and p.b >= d.b
-          #vertical conflict
-          if h and v
-            conflicts.push p
-          #add to conflict list
-        return
-      if conflicts.length
-        console.log d, ' conflicts with ', conflicts
-        rightEdge = d3.max(conflicts, (d2) ->
-          `var maxLabelHeight`
-          `var maxLabelWidth`
-          d2.r
-        )
-        d.l = rightEdge
-        d.x = d.l + bbox.width / 2 + 5
-        d.r = d.l + bbox.width + 10
-      else
-        console.log 'no conflicts for ', d
-      ### add this label to the quadtree, so it will show up as a conflict
-         for future labels.
-      ###
-      labelLayout.add d
-      maxLabelWidth = Math.max(maxLabelWidth, bbox.width + 10)
-      maxLabelHeight = Math.max(maxLabelHeight, bbox.height + 10)
-      return
-    )
-    #.transition()
-    .attr('x', (d) ->
-      d.x
-    ).attr 'y', (d) ->
-      d.y
-    pointers = pointerGroup.selectAll('path.pointer').data(piedata)
-    pointers.enter().append('path').attr('class', 'pointer').style('fill', 'none').style('stroke', 'black').attr 'marker-end', 'url(#circ)'
-    pointers.exit().remove()
-    pointers.transition().attr 'd', (d) ->
-      if d.cx > d.l
-        'M' + d.l + 2 + ',' + d.b + 'L' + d.r - 2 + ',' + d.b + ' ' + d.cx + ',' + d.cy
-      else
-        'M' + d.r - 2 + ',' + d.b + 'L' + d.l + 2 + ',' + d.b + ' ' + d.cx + ',' + d.cy
-    return
+
+        i = d3.interpolate({startAngle: 0,endAngle: 0}, {startAngle: d.startAngle,endAngle: d.endAngle})
+
+        return (t) ->
+            b = i(t);
+            return arc(b)
+
+  renderX3: (data) ->
+
+        if !data
+          data = @get("value")
+        if !data
+          return
+
+        $(@node).children(".title").text($(@node).attr("data-title"))
+        $(@node).children(".more-info").text($(@node).attr("data-moreinfo"))
+        $(@node).children(".updated-at").text(@get('updatedAtMessage'))
+
+        # Build pie
+
+        width = 750
+        height = 450
+        radiuso = 135 #outer radius
+        radiusi = 90 #inner radius
+        radius = Math.min(width, height) / 2 #palette radius min
+        labelRadius = 180
+
+        color = d3.scale.category20()
+
+        $(@node).children("svg").remove();
+
+        arc = d3.svg.arc().outerRadius(radius)
+        pie = d3.layout.pie().value((d) -> d.value)
+
+        svg = d3.select(@node).append("svg:svg")
+          .data([data])
+            .attr("width", width)
+            .attr("height", height)
+          .append("svg:g")
+            .attr("transform", "translate(" + radius + "," + radius + ")")
+
+        arcs = svg.selectAll("g.slice")
+          .data(pie)
+          .enter().append("svg:g").attr("class", "slice")
+
+        arcs.append("svg:path").attr("fill", (d, i) -> color i)
+          .attr("fill-opacity", 0.4).attr("d", arc)
+
+  render: (data) ->
+
+        if !data
+          data = @get("value")
+        if !data
+          return
+
+        $(@node).children(".title").text($(@node).attr("data-title"))
+        $(@node).children(".more-info").text($(@node).attr("data-moreinfo"))
+        $(@node).children(".updated-at").text(@get('updatedAtMessage'))
+
+        # Build pie
+
+        width = 750
+        height = 450
+        radiuso = 135 #outer radius
+        radiusi = 90 #inner radius
+        radius = Math.min(width, height) / 2 #palette radius min
+        labelRadius = 180
+
+        color = d3.scale.category20()
+
+        $(@node).children("svg").remove();
+
+        svg = d3.select(@node).append("svg")
+            .attr("width", width)
+            .attr("height", height)
+            .append("g")
+            .attr("transform", "translate(" + width/2 + "," + height/2 + ")")
+
+        pie = d3.layout.pie()
+            #.sort(null)
+
+        arc = d3.svg.arc()
+          .outerRadius(radiuso)
+          .innerRadius(radiusi)
+
+        piedata = pie(data[0].value)
+        #console.log("update pie", piedata)
+
+        # end Build pie
+
+        #create a marker element if it doesn't already exist
+        defs = svg.select("defs")
+        if defs.empty()
+            defs = svg.append("defs")
+
+        marker = defs.select("marker#circ")
+        if marker.empty()
+            defs.append("marker")
+            .attr("id", "circ")
+            .attr("markerWidth", 6)
+            .attr("markerHeight", 6)
+            .attr("refX", 3)
+            .attr("refY", 3)
+            .append("circle")
+            .attr("cx", 3)
+            .attr("cy", 3)
+            .attr("r", 3)
+
+        #Create/select <g> elements to hold the different types of graphics
+        #and keep them in the correct drawing order
+        pathGroup = svg.select("g.piePaths")
+        if pathGroup.empty()
+            pathGroup = svg.append("g").attr("class", "piePaths")
+
+        pointerGroup = svg.select("g.pointers")
+        if pointerGroup.empty()
+            pointerGroup = svg.append("g").attr("class", "pointers")
+
+        labelGroup = svg.select("g.labels")
+        if labelGroup.empty()
+            labelGroup = svg.append("g").attr("class", "labels")
+
+        path = pathGroup.selectAll("path.pie")
+            .data(piedata)
+
+        path.enter().append("path")
+            .attr("class", "pie")
+            .attr("fill",(d, i) -> return color i)
+
+        #path.transition()
+        #    .duration(1500)
+        #    .attrTween("d", @pieTween);
+
+        #path.exit()
+        #    .transition()
+        #    .duration(300)
+        #    .attrTween("d", @removePieTween)
+        #    .remove();
+
+        path.transition()
+            .duration(1500)
+            #.attrTween("d", pieTween)
+            .attrTween("d", (d,i) ->
+                console.log("d",d)
+                console.log("i",i)
+                i = d3.interpolate({startAngle: 0, endAngle: 0}, {startAngle: d.startAngle, endAngle: d.endAngle})
+                return (t) ->
+                    b = i(t)
+                    return arc(b)
+            )
+
+        path.exit()
+            .transition()
+            .duration(300)
+            .attrTween("d", (d,i) ->
+                i = d3.interpolate({startAngle: d.startAngle,endAngle: d.endAngle},{startAngle: 2 * Math.PI,endAngle: 2 * Math.PI})
+                return (t) ->
+                    b = i(t)
+                    return arc(b)
+            )
+            .remove()
+
+        labels = labelGroup.selectAll("text")
+            .data(piedata.sort((p1,p2) -> return p1.startAngle - p2.startAngle))
+        labels.enter()
+            .append("text")
+            .attr("text-anchor", "middle")
+        labels.exit()
+            .remove()
+
+        labelLayout = d3.geom.quadtree()
+            .extent([[-width,-height], [width,height] ])
+            .x((d) -> return d.x)
+            .y((d) -> return d.y)
+            ([]) #create an empty quadtree to hold label positions
+
+        maxLabelWidth = 0
+        maxLabelHeight = 0
+
+        labels.text((d) ->
+            # Set the text *first*, so we can query the size
+            # of the label with .getBBox()
+            return d.value
+        )
+        .each((d, i) ->
+            # Move all calculations into the each function.
+            # Position values are stored in the data object
+            # so can be accessed later when drawing the line
+
+            # calculate the position of the center marker
+            a = (d.startAngle + d.endAngle) / 2
+
+            #trig functions adjusted to use the angle relative
+            #to the "12 o'clock" vector:
+            d.cx = Math.sin(a) * (radius - 75)
+            d.cy = -Math.cos(a) * (radius - 75)
+
+            # calculate the default position for the label,
+            #   so that the middle of the label is centered in the arc
+            bbox = this.getBBox()
+            #bbox.width and bbox.height will
+            #describe the size of the label text
+            labelRadius = radius - 20
+            d.x =  Math.sin(a) * (labelRadius)
+            d.l = d.x - bbox.width / 2 - 2
+            d.r = d.x + bbox.width / 2 + 2
+            d.y = -Math.cos(a) * (radius - 20)
+            d.b = d.oy = d.y + 5
+            d.t = d.y - bbox.height - 5
+
+            # check whether the default position
+            #   overlaps any other labels
+            conflicts = []
+            #console.log("labelLayout",labelLayout)
+            labelLayout.visit( (node, x1, y1, x2, y2) ->
+                #recurse down the tree, adding any overlapping
+                #node is the node in the quadtree,
+                #node.point is the value that we added to the tree
+                #x1,y1,x2,y2 are the bounds of the rectangle that
+                #this node covers
+
+                #1. left edge of node is to the right of right edge of label
+                #2. right edge of node is to the left of left edge of label
+                #3. top (minY) edge of node is greater than the bottom of label
+                #4. bottom (maxY) edge of node is less than the top of label
+                if  (x1 > d.r + maxLabelWidth/2) or (x2 < d.l - maxLabelWidth/2) or (y1 > d.b + maxLabelHeight/2) or (y2 < d.t - maxLabelHeight/2 )
+                    return true #don't bother visiting children or checking this node
+
+                p = node.point
+                v = false
+                h = false
+                if p #p is defined, i.e., there is a value stored in this node
+                    h =  ( ((p.l > d.l) and (p.l <= d.r)) or ((p.r > d.l) and (p.r <= d.r)) or ((p.l < d.l) and (p.r >=d.r) ) ) #horizontal conflict
+                    v =  ( ((p.t > d.t) and (p.t <= d.b)) or ((p.b > d.t) and (p.b <= d.b)) or ((p.t < d.t) and (p.b >=d.b) ) ) #vertical conflict
+                    if h and v
+                        conflicts.push(p) #add to conflict list
+            )
+
+            if conflicts.length
+                console.log(d, " conflicts with ", conflicts);
+                rightEdge = d3.max(conflicts, (d2) ->
+                    return d2.r
+                )
+
+                d.l = rightEdge
+                d.x = d.l + bbox.width / 2 + 5
+                d.r = d.l + bbox.width + 10
+
+            else console.log("no conflicts for ", d)
+
+            # add this label to the quadtree, so it will show up as a conflict
+            #   for future labels.
+            labelLayout.add( d )
+            maxLabelWidth = Math.max(maxLabelWidth, bbox.width+10)
+            maxLabelHeight = Math.max(maxLabelHeight, bbox.height+10)
+        )
+        .transition() #we can use transitions now!
+        .attr("x", (d) ->
+                    return d.x
+                )
+                .attr("y", (d) ->
+                    return d.y
+                )
+
+        pointers = pointerGroup.selectAll("path.pointer")
+            .data(piedata)
+        pointers.enter()
+            .append("path")
+            .attr("class", "pointer")
+            .style("fill", "none")
+            .style("stroke", "black")
+            .attr("marker-end", "url(#circ)")
+        pointers.exit().remove()
+
+        pointers.transition().attr("d", (d) ->
+            if d.cx > d.l
+                return "M" + (d.l+2) + "," + d.b + "L" + (d.r-2) + "," + d.b + " " + d.cx + "," + d.cy
+            else
+                return "M" + (d.r-2) + "," + d.b + "L" + (d.l+2) + "," + d.b + " " + d.cx + "," + d.cy
+
+        )
+
+  renderX5: (data) ->
+        #console.log("update pie", data);
+
+        if !data
+          data = @get("value")
+        if !data
+          return
+
+        piedata = [data]
+
+        $(@node).children(".title").text($(@node).attr("data-title"))
+        $(@node).children(".more-info").text($(@node).attr("data-moreinfo"))
+        $(@node).children(".updated-at").text(@get('updatedAtMessage'))
+
+        # Build pie
+
+        width = 750
+        height = 450
+        radiuso = 135 #outer radius
+        radiusi = 90 #inner radius
+        radius = Math.min(width, height) / 2 #palette radius min
+        labelRadius = 180
+
+        color = d3.scale.category20()
+
+        $(@node).children("svg").remove();
+
+        #svg = d3.select(@node).append("svg:svg")
+        #    .data([data])
+        #    .attr("width", width)
+        #    .attr("height", height)
+        #    .append("svg:g")
+        #    .attr("transform", "translate(" + width/2 + "," + height/2 + ")")
+        svg = d3.select(@node).append("svg")
+            .data(piedata)
+            .attr("width", width)
+            .attr("height", height)
+            .append("g")
+            .attr("transform", "translate(" + width/2 + "," + height/2 + ")")
+
+        pie = d3.layout.pie()
+            #.sort(null)
+            .value((d) -> d.value)
+
+        arc = d3.svg.arc()
+          .outerRadius(radiuso)
+          .innerRadius(radiusi)
+
+        arcs = svg.selectAll("g.slice")
+            .data(pie)
+            .enter().append("svg:g").attr("class", "slice")
+
+        arcs.append("svg:path").attr("fill", (d, i) -> color i)
+            .attr("fill-opacity", 0.4).attr("d", arc)
+
+        # end Build pie
+        #console.log("update pie", data);
+


Delete client.coffee

Delete client.coffee


updateProviderInto works asyncly and returns promise

--- src/steroids/Providers.coffee
+++ src/steroids/Providers.coffee
@@ -208,8 +208,8 @@
           sandboxDB.createSandboxDBConfig("#{bucket.login}#{bucket.password}", bucket.name, bucket.datastore_bucket_id)
       ).then (data) =>
         console.log "Local config file created at #{chalk.bold("config/sandboxdb.yaml")}"
-        @updateProviderInfo(provider)
-        deferred.resolve()
+        @updateProviderInfo(provider).then ()->
+          deferred.resolve()

     deferred.promise

@@ -378,23 +378,28 @@
   # helpers

   updateProviderInfo: (provider) =>
+    deferred = q.defer()
     config = getConfig()

-    data =
-      providerTypeId: 6,
-      name: 'appgyver_sandbox'
-      configurationKeys:
+    data = provider
+
+    unless data.configurationKeys?
+      data.configurationKeys =
         bucket_id: config['bucket_id']
         steroids_api_key: config['apikey']

     console.log "Updating SandboxDB data provider information..."

-    @config_api.put("/app/#{@getAppId()}/service_providers/#{provider}.json", data, (err, req, res, obj) =>
-      @config_api.close()
-      console.log 'Done, all good!'
+    @config_api.put("/app/#{@getAppId()}/service_providers/#{provider.uid}.json", data, (err, req, res, obj) =>
+      console.log 'SandboxDB data provider information was updated.'
+      deferred.resolve()
+      @config_api.close()
       # restify does not close...
-      process.exit 1
-    )
+      # TODO: dig into exiting process
+      #process.exit 1
+    )
+
+    deferred.promise

   saveRamlToFile: () =>
     @config_api.headers["Accept"] = "text/yaml"


postmixin hooks, validation, naming

postmixin hooks, validation, naming

--- src/mixinfactory.coffee
+++ src/mixinfactory.coffee
@@ -20,9 +20,18 @@
     string_keys = _.without(@mixin_keys, 'name')
     "Mixin(#{@name}: #{string_keys.join(', ')})"

-  @make_mixin: (obj) ->
+  @postmixin_hooks = [
+    'post_mixinproto'
+    'post_mixinclass'
+  ]
+
+  @validate_mixin: (mixin) ->
+    unless mixin instanceof @
+      throw new TypeError "Expected a Mixin instance"
+
+  @from_obj: (obj) ->
     unless _.isObject(obj) && !_.isArray(obj)
-      throw new TypeError "Expected non-empty mixin object"
+      throw new TypeError "Expected non-empty object"
     unless _.isString(obj.name) && obj.name
       throw new @ArgumentError "Expected String name in options argument"


home/stacks: switch over isVisible prop

home/stacks: switch over isVisible prop

--- client/home/lib/stacks/components/stacktemplateitem/index.coffee
+++ client/home/lib/stacks/components/stacktemplateitem/index.coffee
@@ -7,17 +7,18 @@

   renderButton: ->

-    { template, onAddToSidebar, onRemoveFromSidebar } = @props
+    { template, onAddToSidebar, onRemoveFromSidebar, isVisibleOnSidebar } = @props

-    unless template.get 'inUse'
-      return ADD TO SIDEBAR
-
-    return REMOVE FROM SIDEBAR
+    if isVisibleOnSidebar
+      REMOVE FROM SIDEBAR
+    else
+      ADD TO SIDEBAR

   render: ->

     { template, onOpen } = @props
+
     editorUrl = "/Stack-Editor/#{template.get '_id'}"


Add docblocks for `Notification` public functions

* Mark `dismiss` as an "extended" API because its use case is uncommon.
* Mark event handler functions as public because responding to a
  notification being displayed or dismissed is useful.

--- src/notification.coffee
+++ src/notification.coffee
@@ -19,18 +19,40 @@
     unless _.isObject(@options) and not _.isArray(@options)
       throw new Error("Notification must be created with an options object: #{@options}")

+  ###
+  Section: Event Subscription
+  ###
+
+  # Public: Invoke the given callback when the notification is dismissed.
+  #
+  # * `callback` {Function} to be called when the notification is dismissed.
+  #
+  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
   onDidDismiss: (callback) ->
     @emitter.on 'did-dismiss', callback

+  # Public: Invoke the given callback when the notification is displayed.
+  #
+  # * `callback` {Function} to be called when the notification is displayed.
+  #
+  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
   onDidDisplay: (callback) ->
     @emitter.on 'did-display', callback

   getOptions: -> @options

+  ###
+  Section: Methods
+  ###
+
   # Public: Retrieves the {String} type.
+  #
+  # Returns a {String}.
   getType: -> @type

   # Public: Retrieves the {String} message.
+  #
+  # Returns a {String}.
   getMessage: -> @message

   getTimestamp: -> @timestamp
@@ -42,6 +64,8 @@
       and @getType() is other.getType() \
       and @getDetail() is other.getDetail()

+  # Extended: Dismisses the notification, removing it from the UI. This will call all callbacks
+  # added via `onDidDismiss`.
   dismiss: ->
     return unless @isDismissable() and not @isDismissed()
     @dismissed = true


Adding additional character formatting.

Adding additional character formatting.

--- src/scripts/news.coffee
+++ src/scripts/news.coffee
@@ -18,7 +18,7 @@
         strings.push "Here's the latest news headlines:\n"

       for story in response.responseData.results
-        strings.push story.titleNoFormatting.replace("'", "'").replace(""", "\"")
+        strings.push story.titleNoFormatting.replace("'", "'").replace("`", "'").replace(""", "\"")
         strings.push story.unescapedUrl + "\n"

       msg.send strings.join "\n"


Add pigments to config

Add pigments to config

--- Atom/config.cson
+++ Atom/config.cson
@@ -20,3 +20,5 @@
     js_space_after_anon_function: true
     language_js_beautify_on_save: true
     js_end_with_newline: true
+  "pigments":
+    markerType: "dot"


fixing issue where partial view helper didn't pass through _app context

--- shared/handlebars_helpers.coffee
+++ shared/handlebars_helpers.coffee
@@ -37,6 +37,9 @@
       else
         options

+    data = _.clone(data)
+    data._app = @_app
+
     html = template(data)
     new Handlebars.SafeString(html)


prevent multiple content displays from opening when switching videos

--- client/app/Applications/Activity.kdapplication/ContentDisplays/tutorial.coffee
+++ client/app/Applications/Activity.kdapplication/ContentDisplays/tutorial.coffee
@@ -214,10 +214,12 @@
         if @after.length >0
           @listAnchorNext.addSubView new TutorialListSwitchBox
             direction:"next"
+            delegate:@
           , @after[0]
         if @before.length >0
           @listAnchorPrevious.addSubView new TutorialListSwitchBox
             direction:"previous"
+            delegate:@
           , @before[@before.length-1]

@@ -336,6 +338,7 @@

     @outgoingContainer = new KDView
       click:=>
+        @getSingleton("contentDisplayController").emit "ContentDisplayWantsToBeHidden", @getDelegate()
         appManager.tell "Activity", "createContentDisplay", @getData()


groups/profile customise: fixed color picker

--- client/app/Applications/Groups.kdapplication/Views/groupscustomizeviews.coffee
+++ client/app/Applications/Groups.kdapplication/Views/groupscustomizeviews.coffee
@@ -410,6 +410,7 @@
     else if @type is 'customColor'
       @color = new StaticGroupBackgroundColorPickerView
         cssClass : 'custom-color-picker'
+        delegate : @getDelegate()
       ,data

   click: ->


Add a validation checker for time entry

Add a validation checker for time entry

--- test/models/time_entry.coffee
+++ test/models/time_entry.coffee
@@ -5,7 +5,6 @@
   t = new TimeEntry
     start: new Date() - 5
     end: new Date() - offset
-    duration: 60 * 60
     message: 'message goes here'
     userId: 'someuserID'
     projectId: 'someprojectID'
@@ -23,3 +22,12 @@
     t.message.should.be.equal 'message goes here'
     t.validate().should.be.true

+  it 'should not validate', ->
+    t = new TimeEntry
+      start: 'not a date'
+      end: 'not a date either'
+      message: 'he he i have invalid dates'
+      userId: 'jokster1'
+      projectId: 'alternate time continuum'
+    t.validate().should.be.false
+


Backfill spec coverage for handling highlighter change events.

--- spec/atom/editor-spec.coffee
+++ spec/atom/editor-spec.coffee
@@ -45,6 +45,11 @@
         buffer.insert([0, 4], "g")
         expect(editor.lines.find('.line:eq(0) span:eq(0)')).toMatchSelector '.keyword.definition'

+        # verify that re-highlighting can occur below the changed line
+        buffer.insert([5,0], "/* */")
+        buffer.insert([1,0], "/*")
+        expect(editor.lines.find('.line:eq(2) span:eq(0)')).toMatchSelector '.comment'
+
   describe "cursor movement", ->
     describe ".setCursorPosition({row, column})", ->
       beforeEach ->


Whitespace

Whitespace

--- src/test/test-creation.coffee
+++ src/test/test-creation.coffee
@@ -30,8 +30,6 @@
       helpers.assertFiles expected
       done()

-
-
 describe "Subgenerator subgenerator", ->
   beforeEach (done) =>
     helpers.testDirectory path.join(__dirname, "temp"), (err) =>


Add simple test of activation

Add simple test of activation

This ensures activate is called by at least one spec

--- spec/github-file-spec.coffee
+++ spec/github-file-spec.coffee
@@ -313,3 +313,15 @@

       githubFile.gitUrl = -> "git@github.com:/foo/bar.git"
       expect(githubFile.githubRepoUrl()).toBe "http://github.com/foo/bar"
+
+  it "activates when a command is triggered on the active editor", ->
+    activationPromise = atom.packages.activatePackage('open-on-github')
+
+    waitsForPromise ->
+       atom.workspace.open()
+
+    runs ->
+      atom.commands.dispatch(atom.views.getView(atom.workspace), 'open-on-github:file')
+
+    waitsForPromise ->
+      atom.packages.activatePackage('open-on-github')


Trigger focusout event on hidden input

Trigger focusout event on hidden input

--- spec/select-list-spec.coffee
+++ spec/select-list-spec.coffee
@@ -160,7 +160,7 @@
   describe "when the mini editor loses focus", ->
     it "triggers the cancelled hook and detaches the select list", ->
       spyOn(selectList, 'detach')
-      miniEditor.trigger 'focusout'
+      miniEditor.hiddenInput.trigger 'focusout'
       expect(selectList.cancelled).toHaveBeenCalled()
       expect(selectList.detach).toHaveBeenCalled()


Enable all unit tests again.

Enable all unit tests again.

--- test/tests/ContextTest.coffee
+++ test/tests/ContextTest.coffee
@@ -24,7 +24,7 @@
 	# Todo: event cleanup
 		# Todo: parent layer on context cleanup

-	describe.only "Freezing", ->
+	describe "Freezing", ->

 		it "should remove events", ->


Refactor the test case into smaller units.

--- test/unit/servicesSpec.coffee
+++ test/unit/servicesSpec.coffee
@@ -60,87 +60,110 @@

   describe 'Intensity Service', ->
-    # The qiprofile Intensity factory.
-    Intensity = null
+    # The dummy input session.
+    session = null
+    # The configuration.
+    config = null
+    # The x axis values are the time points.
+    expectedX = [1..32]

     beforeEach ->
       # Enable the test services.
-      inject ['Intensity', (_Intensity_) ->
-        Intensity = _Intensity_
+      inject ['Intensity', (Intensity) ->
+        # The dummy input session.
+        session =
+          scan:
+            title: 'Scan'
+            intensity:
+              # Max intensity is at session 10.
+              intensities: (30 - Math.abs(10 - i) for i in [1..32])
+          registrations: [
+            name: 'reg_01'
+            title: 'Realigned'
+            intensity:
+              # Dampen the registration intensity a bit.
+              intensities: (30 - (Math.abs(10 - i) * 1.2) for i in [1..32])
+          ]
+        # Configure the chart.
+        config = Intensity.configureChart(session)
       ]

-    it 'should configure the chart', ->
-      # Make the dummy input session.
-      session =
-        scan:
-          intensity:
-            # Max intensity is at session 10.
-            intensities: (30 - Math.abs(10 - i) for i in [1..32])
-        registrations: [
-          name: 'reg_01'
-          intensity:
-            # Dampen the registration intensity a bit.
-            intensities: (30 - (Math.abs(10 - i) * 1.2) for i in [1..32])
-        ]
-      # Configure the chart.
-      config = Intensity.configureChart(session)
+    it 'should configure two time series', ->
       expect(config.data).to.exist
       expect(config.data.length).to.equal(2)
+
+    it 'should set the x axis values to the time points', ->
+      expect(config.xValues).to.eql(expectedX)

-      # Verify the scan coordinates.
-      expectedX = [1..32]
-      scan = _.find config.data, (dataSeries) -> dataSeries.key == 'Scan'
-      expect(scan).to.exist
-      expect(scan.values).to.exist
-      scanX = (coord[0] for coord in scan.values)
-      expect(scanX).to.eql(expectedX)
-      scanY = (coord[1] for coord in scan.values)
-      expect(scanY).to.eql(session.scan.intensity.intensities)
+    describe 'Scan Configuration', ->
+      scan = null

-      # Verify the registration coordinates.
-      reg = _.find config.data, (dataSeries) -> dataSeries.key == 'Realigned'
-      expect(reg).to.exist
-      expect(reg.values).to.exist
-      regX = (coord[0] for coord in reg.values)
-      expect(regX).to.eql(expectedX)
-      regY = (coord[1] for coord in reg.values)
-      expect(regY).to.eql(session.registrations[0].intensity.intensities)
-
-      # Verify the x-axis values.
-      expect(config.xValues).to.eql(expectedX)
+      beforeEach ->
+        scan = config.data[0]
+
+      it 'should set the key to the title', ->
+        expect(scan.key).to.equal('Scan')
+
+      it 'should have values', ->
+        expect(scan.values).to.exist
+
+      it 'should configure the coordinates', ->
+        scanX = (coord[0] for coord in scan.values)
+        expect(scanX).to.eql(expectedX)
+        scanY = (coord[1] for coord in scan.values)
+        expect(scanY).to.eql(session.scan.intensity.intensities)
+
+    describe 'Registration Configuration', ->
+      reg = null
+
+      beforeEach ->
+        reg = config.data[1]
+
+      it 'should set the key to the title', ->
+        expect(reg.key).to.equal('Realigned')
+
+      it 'should have values', ->
+        expect(reg.values).to.exist
+
+      it 'should configure the coordinates', ->
+        scanX = (coord[0] for coord in reg.values)
+        expect(scanX).to.eql(expectedX)
+        scanY = (coord[1] for coord in reg.values)
+        expect(scanY).to.eql(session.registrations[0].intensity.intensities)

   describe 'Imaging Profile', ->
-    # The qiprofile Modeling factory.
-    Modeling = null
+    # The input session.
+    session = null
+    # The configuration.
+    config = null

     beforeEach ->
       # Enable the test services.
-      inject ['Modeling', (_Modeling_) ->
-        Modeling = _Modeling_
+      inject ['Modeling', (Modeling) ->
+        # The mock input.
+        subject =
+          sessions: [
+            acquisition_date: new Date
+            modeling:
+              v_e: Math.random()
+              tau_i: Math.random()
+              fxl_k_trans: Math.random()
+              fxr_k_trans: Math.random()
+              delta_k_trans: Math.random()
+            number: 1
+          ]
+        session = subject.sessions[0]
+        config = Modeling.configureTable(subject.sessions)
       ]

-    it 'should configure the imaging profile table', ->
-      # The mock input.
-      subject =
-        sessions:
-          [
-            {
-              acquisition_date: new Date
-              modeling: {
-                v_e: Math.random()
-                tau_i: Math.random()
-                fxl_k_trans: Math.random()
-                fxr_k_trans: Math.random()
-                delta_k_trans: Math.random()
-              }
-              number: 1
-            }
-          ]
-      # The expected result.
-      config = Modeling.configureTable(subject.sessions)
+    it 'should configure one session', ->
       expect(config.data).to.exist
       expect(config.data.length).to.equal(1)
+
+    it 'should initialize the open flags to true', ->
       expect(config.ktransOpen).to.equal(true)
       expect(config.veOpen).to.equal(true)
       expect(config.tauiOpen).to.equal(true)
+
+    # TODO - add percent change test cases.


Do not bould after creating platform

Do not bould after creating platform

--- src/tasks/build/base/platform.coffee
+++ src/tasks/build/base/platform.coffee
@@ -8,7 +8,8 @@
     helpers.exec "phonegap remote build #{platform} #{helpers.setVerbosity()}", fn

   local = (platform, fn) ->
-    helpers.exec "cordova platform add #{platform} #{helpers.setVerbosity()} && cordova build #{platform} #{helpers.setVerbosity()}", fn
+    # helpers.exec "cordova platform add #{platform} #{helpers.setVerbosity()} && cordova build #{platform} #{helpers.setVerbosity()}", fn
+    helpers.exec "cordova platform add #{platform} #{helpers.setVerbosity()}", fn

   runAfter = (provider, platform, fn) ->
     adapter = path.join __dirname, '..', 'after', provider, "#{platform}.js"


Missed 'log' dependency in input/parser added

--- input/parser.coffee
+++ input/parser.coffee
@@ -1,5 +1,6 @@
 'use strict'

+log = require 'neft-log'
 bindingParser = require 'neft-binding/parser'

 {BINDING_THIS_TO_TARGET_OPTS} = bindingParser


Added Locks on properties

Added Locks on properties

renderers can bind to _<propertyname>_locked if they want to take
action when something is locked/unlocked

--- BaseClass.coffee
+++ BaseClass.coffee
@@ -13,6 +13,7 @@
             'use strict'
             if arguments.length and "object" is typeof arguments[0]
                 @[key] = prop for key, prop of arguments[0]
+            @_locked = {}
             @_watchers = (prop, oldValue, newValue, index, self) ->
                 if callbacks = @_watchers[key = '_' + prop]?.slice()
                     for callback in callbacks
@@ -33,7 +34,7 @@
         is: (name) ->
             'use strict'
             name = name.declaredClass if "object" is typeof name and name?.declaredClass
-            name = name.classname if "function" is typeof name
+            name = name.classname if _lang.isFunction name
             return true if name in @_fullname()
             return true if name in @_implements()
             false
@@ -42,18 +43,18 @@
             not @is name
         set: (prop, value, index, self) ->
             'use strict'
-            throw new Exception("IllegalPropertyNameException", "No property " + prop + " on class " + this.declaredClass) if this[prop] is undefined
+            throw new Exception("IllegalPropertyNameException", "No property " + prop + " on class " + @declaredClass) if this[prop] is undefined
             oldValue = this[prop]
             newValue = _lang.clone value
-            if 1 is (cancel = this._validators(prop, oldValue, newValue, index, self))
+            if 1 is (cancel = @_validators(prop, oldValue, newValue, index, self))
                 return oldValue

             if index? then this[prop][index] = newValue else this[prop] = newValue
-            this._watchers(prop, oldValue, newValue, index, self)
+            @_watchers(prop, oldValue, newValue, index, self)
             newValue
         get: (prop) ->
             'use strict'
-            throw new Exception("IllegalPropertyNameException", "No property " + prop + " on class " + this.declaredClass) if this[prop] is undefined
+            throw new Exception("IllegalPropertyNameException", "No property " + prop + " on class " + @declaredClass) if this[prop] is undefined
             this[prop]
         watch: (prop, callback) ->
             'use strict'
@@ -65,28 +66,56 @@
                 remove: () ->
                     callbacks.splice(callbacks.indexOf(callback), 1)
             }
-        validate: (prop, callback) ->
+        validate: (prop, callback, _first) ->
             'use strict'
             key = '_' + prop
             callbacks = @_validators[key]
             callbacks = @_validators[key] = [] if typeof callbacks isnt "object"
-            callbacks.push(callback)
+            if _first
+                callbacks.splice(0, 0, callback)
+            else
+                callbacks.push(callback)
             return {
                 remove: () ->
                     callbacks.splice(callbacks.indexOf(callback), 1)
             }
         inherited: () ->
-            fname = this.inherited.caller.nom
-            args = this.inherited.caller.arguments
-            classname = this.inherited.caller.cls
-            passed = this.declaredClass is classname
-            ctx = this.__super__
+            fname = @inherited.caller.nom
+            args = @inherited.caller.arguments
+            classname = @inherited.caller.cls
+            passed = @declaredClass is classname
+            ctx = @__super__
             max = 20
             ctx = ctx.__super__ until not ctx? or
                 (ctx[fname]? and (passed=passed or classname is ctx.declaredClass) and classname isnt ctx.declaredClass)
             #ctx = ctx.__super__ until not ctx? or (ctx.hasOwnProperty(fname) and (passed=passed or classname is ctx.declaredClass) and classname isnt ctx.declaredClass)
             throw new Exception("NullPointerException", classname + " has no __super__ with function " + fname) if not ctx?
             return ctx[fname].apply(this, args) if ctx?
+        lock: (props, deferred) ->
+            'use strict'
+            throw new Exception("IllegalArgumentsException", "lock must take a deferred as second argument") if not deferred? or not deferred.addBoth?
+            props = [props] if not _lang.isArray props
+            for prop in props
+                if not @islocked prop
+                    deferred.addBoth _lang.hitch this, ()->
+                        @unlock prop
+                    @_locked[prop] = @validate prop, (prop, oldValue, newValue) ->
+                        console.warn "Trying to set locked property '" + prop + "'' on " + @declaredClass
+                        1
+                    , _first = true
+                    @["_" + prop + "_locked"] = false if not @["_" + prop + "_locked"]?
+                    @set("_" + prop + "_locked", true)
+        unlock: (props) ->
+            'use strict'
+            props = [props] if not _lang.isArray props
+            for prop in props
+                if @islocked prop
+                    @_locked[prop].remove()
+                    delete @_locked[prop]
+                    @set("_" + prop + "_locked", false) if @["_" + prop + "_locked"]?
+        islocked: (prop) ->
+            'use strict'
+            not not @_locked[prop]?
         toString: () ->
             'use strict'
             @declaredClass


Update zh-HANT.coffee (#6173)

Update zh-HANT.coffee (#6173)

修改 become_investor

--- app/locale/zh-HANT.coco.coffee
+++ app/locale/zh-HANT.coco.coffee
@@ -5,7 +5,7 @@
 #    meta_keywords: "CodeCombat, python, javascript, Coding Games"
     meta_description: "通過遊戲來學習程式編碼。 學習 Python、JavaScript 和 HTML，解決其中的難題並學會製作自己的遊戲和網站。"
 #    meta_og_url: "https://codecombat.com"
-#    become_investor: "to become an investor in CodeCombat"
+    become_investor: "成為CodeCombat的投資人"
     built_for_teachers_title: "一個由教師建構的編碼遊戲"
     built_for_teachers_blurb: "教孩子們編寫代碼往往會令人感到招架不住。 透過 CodeCombat 可以幫助所有教育工作者教授學生如何使用 JavaScript 或 Python（兩種最流行的編程語言）進行編碼。 透過包含六個計算機科學的綜合課程，以及遊戲開發和 Web 開發專題來進行加強學習，孩子們將在從基本語法往遞迴程式設計中前進！"
     built_for_teachers_subtitle1: "電腦科學"


updated subject set list when uploading subjects

--- app/pages/lab/subject-set.cjsx
+++ app/pages/lab/subject-set.cjsx
@@ -71,8 +71,7 @@
     subjects: null

   componentWillReceiveProps: (nextProps) ->
-    if @props.subjectSet.id isnt nextProps.subjectSet.id
-      @setSubjectResources(nextProps.subjectSet.id, "1")
+    @setSubjectResources(nextProps.subjectSet.id, "1")

   componentWillMount: ->
     @setSubjectResources(@props.subjectSet.id, @state.page)


For section routes, set the page resource controller’s boundsRect property to the section bounds

--- app/routes/page/section.coffee
+++ app/routes/page/section.coffee
@@ -17,7 +17,7 @@
     page = @modelFor 'page'
     controller.set 'page', page
     controller.set 'section', model
-    controller.set 'controllers.page.bounds', model.get('bounds')
+    controller.set 'controllers.page.boundsRect', model.get('bounds')

   renderTemplate: ->
     @_super()


data.yaml loading of sub-files needs to be relative to data.yaml, not the executable's directory

--- src/stubs-portal.coffee
+++ src/stubs-portal.coffee
@@ -37,11 +37,9 @@
    context = null

    before (done) ->
-      stubby = spawn command, (args.concat ['-d', 'data.yaml']), {
-         cwd: path.resolve __dirname, '../data'
+      stubby = spawn command, (args.concat ['-d', (path.resolve __dirname, "../data/data.yaml")]), {
          env: process.env
       }
-      stubby.stderr.on 'data', (data) -> console.log data.toString()
       poller = ->
          req = http.request {
             port: port


Remove Tres.Notifier. Name classes more descriptively

--- assets/javascripts/tres.coffee
+++ assets/javascripts/tres.coffee
@@ -102,10 +102,10 @@
   activate: ->
     if not @modal
       $body.find('>section')
-        .removeClass('current')
+        .removeClass('current-screen')
         .css '-webkit-transform', ''
-    @$el.addClass 'current'
-    @$el.addClass 'modal' if @modal
+    @$el.addClass 'current-screen'
+    @$el.addClass 'modal-screen' if @modal
     @$el.css '-webkit-transform': 'none'
     @active.apply @, arguments if _.isFunction @active
     $window.trigger 'resize'
@@ -217,23 +217,12 @@

 # ---

-# Handles displaying in-app notifications
-Tres.Notifier =
-  $el: $('

')
-
-  notify : (message, options = { duration : 5000, type : 'exclamation-sign' }) ->
-    @$el.appendTo $body
-    $li = $ make 'li', { class: "icon-#{options.type}" }, message
-    @$el.append $li
-    $li.slideDown 250, =>
-      _.delay =>
-        $li.slideUp => $li.remove()
-      , options.duration
-
-# ---
-
-# Just a regular Backbone.Router for now. You'll have one for application
+# Just a regular Backbone.Router for now. You'll have one for application.
 class Router extends Backbone.Router
+
+  # Shorthand for going to a URL and triggering it.
+  go: (url) ->
+    @navigate url, trigger: yes

 Tres.Router = new Router


improve autocomplete collection

improve autocomplete collection

--- app/assets/javascripts/active_admin/lib/jquery.autocomplete_collection.js.coffee
+++ app/assets/javascripts/active_admin/lib/jquery.autocomplete_collection.js.coffee
@@ -1,4 +1,4 @@
-AutocompleteCollection = (element, options) ->
+@AutocompleteCollection = (element, options) ->
   @options = $.extend({}, $.fn.autocomplete_collection.defaults, options)
   @resource = @options.resource or @options.method
   @$element = $(element)
@@ -94,7 +94,7 @@
       .addClass("autocomplete-collection-collection-item")
       .attr('data-autocomplete-collection-value', val[@options.property])
       .attr('data-autocomplete-collection-id', val.id)
-      .html val[@options.property]
+      .html("#{val[@options.property]}")
     $x = $(document.createElement("a"))
       .addClass("autocomplete-collection-remove-item")
       .html("×")
@@ -104,8 +104,8 @@
     @$element.val ""

   setValue: ->
+    @$element.data('collection', @collection)
     @$hidden.val JSON.stringify @collection
-    # @$hidden.val $.map(@collection, (o) -> "'#{o.id.toString()}'" ).join(",")

   collectionAdd: (val) ->
     @collection.push(val)
@@ -113,18 +113,6 @@

   collectionRemove: (i) ->
     @collection.splice i, 1
-    @setValue()
-
-  collectionReorder: (ids) ->
-    that = @
-    ordered_collection = []
-    for id, i in ids
-      do (id) ->
-        index = that.indexInCollection(id)
-        val = that.collection[index]
-        val.position = i + 1
-        ordered_collection.push val
-    @collection = ordered_collection
     @setValue()

   add: (val) ->
@@ -143,17 +131,30 @@
         $item.remove()

   build: ->
-    that = @
-    $.each @collection, (i, val) ->
-      that.draw(val)
+    $.each @collection, (i, val) =>
+      @draw(val)
     @setValue()
-    if $.fn.sortable?
-      @$collection.sortable
-        stop: (e, ui) ->
-          collectionIds = that.$collection.find("[data-autocomplete-collection-id]").map( ->
-            parseInt $(@).attr("data-autocomplete-collection-id")
-          ).get()
-          that.collectionReorder(collectionIds)
+    return if !$.fn.sortable
+    @$collection.sortable
+      stop: $.proxy(@collectionReorder, @)
+
+  collectionReorder: (e, ui) ->
+    orderedCollection = []
+    for li, i in @$collection.children()
+      do (li) =>
+        name = $(li).find(".ac-prop-val").text()
+        items = $.grep @collection, (itm) -> itm.name is name
+        return if !items.length
+        item = items[0]
+        item.position = i + 1
+        orderedCollection.push item
+    orderedCollection.sort (a, b) ->
+      if a.position  b.position then 1 else 0
+    @collection = orderedCollection
+    @setValue()

   lookup: (event) ->
     that = @
@@ -219,12 +220,12 @@
         item = item[that.options.property]
         i.find('a').html(that.highlighter(item))
         return i[0]
-    items.first().addClass('active')
     @$menu.html(items)
     if @allowNew
-      $addNewLi = $(@options.item)
+      $addNewLi = $(@options.item).prop('data-value', @query)
       $addNewLi.find("a").addClass('add-new icon-plus').html("Add new item")
       @$menu.append $addNewLi
+    @$menu.children().first().addClass('active')
     @

   next: (event) ->


Simplify index calculation.

Simplify index calculation.

--- background_scripts/main.coffee
+++ background_scripts/main.coffee
@@ -360,8 +360,7 @@
           when "last"
             tabs.length - 1
       # Bring toSelect into the range [0,tabs.length).
-      toSelect += tabs.length while toSelect < 0
-      toSelect %= tabs.length
+      toSelect = (toSelect + tabs.length * Math.abs count) % tabs.length
       chrome.tabs.update tabs[toSelect].id, selected: true

 updateOpenTabs = (tab, deleteFrames = false) ->


Removed overhead.

Removed overhead.

--- app/assets/javascripts/app/models/postmaster_filter.coffee
+++ app/assets/javascripts/app/models/postmaster_filter.coffee
@@ -8,7 +8,7 @@
     { name: 'channel',        display: 'Channel',           type: 'input', readonly: 1 },
     { name: 'match',          display: 'Match all of the following',      tag: 'postmaster_match' },
     { name: 'perform',        display: 'Perform action of the following', tag: 'postmaster_set' },
-    { name: 'note',           display: 'Note',              tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
+    { name: 'note',           display: 'Note',              tag: 'textarea', limit: 250, null: true },
     { name: 'updated_at',     display: 'Updated',           tag: 'datetime', readonly: 1 },
     { name: 'active',         display: 'Active',            tag: 'active', default: true },
     { name: 'created_by_id',  display: 'Created by',        relation: 'User', readonly: 1 },


fix grunt task lang

fix grunt task lang

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -130,8 +130,15 @@

   grunt.registerTask 'lang', 'Make lang file.', ()->
     grunt.log.writeln 'processing..'
-    done = this.async;
+    done = @async
     fs = require('fs')
+
+    make_repl = (lang) ->
+      new RegExp('^[\\*\\s]*\\[lang:' + lang + '\\]\\n([\\s\\S]*?)^[\\*\\s]*\\[\\/lang\\]\\n', 'mg')
+
+    repl_en = make_repl 'en'
+    repl_ja = make_repl 'ja'
+    repl_de = make_repl 'de'

     (glob.sync "dev/{plugins\/,}*.js").forEach (source)->
       dist_en = source.toString().replace(/dev\//, './')
@@ -139,19 +146,19 @@
       dist_de = source.toString().replace(/dev\//, './de/')

       fs.writeFileSync dist_en, (fs.readFileSync source).toString()
-      .replace(/^[\*\s]+\[lang:en\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*\n/mg, '$1')
-      .replace(/^[\*\s]+\[lang:ja\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*\n/mg, '')
-      .replace(/^[\*\s]+\[lang:de\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*\n/mg, '')
+        .replace(repl_en, '$1')
+        .replace(repl_ja, '')
+        .replace(repl_de, '')

       fs.writeFileSync dist_ja, (fs.readFileSync source).toString()
-      .replace(/^[\*\s]+\[lang:en\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*/mg, '')
-      .replace(/^[\*\s]+\[lang:ja\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*/mg, '$1')
-      .replace(/^[\*\s]+\[lang:de\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*/mg, '')
+        .replace(repl_en, '')
+        .replace(repl_ja, '$1')
+        .replace(repl_de, '')

       fs.writeFileSync dist_de, (fs.readFileSync source).toString()
-      .replace(/^[\*\s]+\[lang:en\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*\n/mg, '')
-      .replace(/^[\*\s]+\[lang:ja\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*\n/mg, '')
-      .replace(/^[\*\s]+\[lang:de\]\n(\s*[\s\S]*?\n)[\*\s]*\[\/lang\]\s*\n/mg, '$1')
+        .replace(repl_en, '')
+        .replace(repl_ja, '')
+        .replace(repl_de, '$1')

       grunt.log.writeln(source)
     grunt.log.writeln 'processing..'


Fix Jade compilation

Fix Jade compilation

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -149,7 +149,7 @@
     watch:
       jade:
         files: ["src/**/*.jade"]
-        tasks: ["newer:jade:dev"]
+        tasks: ["jade:dev"]

       stylus:
         files: ["src/stylus/*.styl"]


Now use by default uglified libraries on development process.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,17 +1,7 @@
 module.exports = (grunt) ->
     externalSources = [
-        'app/js/jquery.js'
-        'app/js/ui/jquery.ui.core.js'
-        'app/js/ui/jquery.ui.position.js'
-        'app/js/ui/jquery.ui.widget.js',
-        'app/js/ui/jquery.ui.mouse.js',
-        'app/js/ui/jquery.ui.draggable.js',
-        'app/js/ui/jquery.ui.droppable.js',
-        'app/js/ui/jquery.ui.effect.js',
-        'app/js/ui/jquery.ui.effect-drop.js',
-        'app/js/ui/jquery.ui.effect-transfer.js',
-        'app/js/ui/jquery.ui.position.js',
-        'app/js/ui/jquery.ui.sortable.js',
+        'app/js/jquery.js',
+        'app/js/ui/jquery-ui.js',
         'app/js/jquery.markitup.js',
         'app/js/markdown.js',
         'app/js/lodash.js',
@@ -104,7 +94,7 @@

             libs: {
                 files: externalSources,
-                tasks: ["concat"],
+                tasks: ["uglify"],
             }
         },

@@ -157,7 +147,7 @@
     grunt.registerTask('dev', [
         'less',
         'coffee:dev',
-        'concat:libs',
+        'uglify:libs',
         'htmlmin',
     ])


Update node-webkit version for builder

Update node-webkit version for builder

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -3,7 +3,7 @@
     nodewebkit:
       options:
         build_dir: './builds'
-        version: '0.8.3'
+        version: '0.8.4'
         mac: true
         win: true
         linux32: false


Remove unnecessary test clean

Remove unnecessary test clean

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -24,8 +24,6 @@
     pkg: grunt.file.readJSON 'package.json'

     clean:
-      test:
-        ['test/specs/**/*.js']
       prisoner:
         ['prisoner.js', 'prisoner.min.js']

@@ -65,7 +63,6 @@
           'prisoner.js': 'src/prisoner.coffee'

   grunt.registerTask 'test', [
-    'clean:test'
     'webpack:test'
     'mocha_phantomjs'
   ]


Add focus colors to colors.css.

Add focus colors to colors.css.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -59,7 +59,7 @@
             options = @options()
             cssOutput = ''
             for background in [true, false]
-                for hover in [true, false]
+                for pseudo in ['hover', 'focus', '']
                     for light in [true, false]
                         cssOutput += "\n" + (grunt.template.process(
                             cssTemplate,
@@ -68,8 +68,8 @@
                                 color: if light then ColorMatcher.rgba(r, g, b, "0.30") else ColorMatcher.rgb(r, g, b)
                                 background: if background then "background-" else ""
                                 light: if light then "light-" else ""
-                                hover: if hover then "hover-" else ""
-                                hoverPc: if hover then ":hover" else "") for own key, [r, g, b] of ColorMatcher.serviceColors).join "\n"
+                                hover: if pseudo == '' then "" else "hover-"
+                                hoverPc: if pseudo != '' then ":" + pseudo) for own key, [r, g, b] of ColorMatcher.serviceColors).join "\n"

             grunt.file.write options.output, cssOutput + "\n"
             return


Clean .tmp directory on build

Clean .tmp directory on build

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -21,6 +21,7 @@
     clean:
       [
         'dist/**/*'
+        '.tmp/**/*'
       ]

     # Copy static file to dist


Start with first view

Start with first view

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -23,7 +23,7 @@
           base: './examples/'
           middleware: (connect, options) ->
             [connect.static(options.base), (req, res, next) ->
-              fs.readFile "#{options.base}/3_stacked_modal_with_marionette.html", (err, data) ->
+              fs.readFile "#{options.base}/1_single_view.html", (err, data) ->
                 res.writeHead(200)
                 res.end(data)
             ]


Update formatting of fs check message

Update formatting of fs check message

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -313,11 +313,11 @@
 		checkFS: (callback = (error) ->) ->
 			Settings = require "settings-sharelatex"
 			if Settings.filestore.backend=="fs"
-				grunt.log.write "Checking FS configuration..."
+				grunt.log.write "Checking FS configuration... "
 				fs = require("fs")
 				fs.exists Settings.filestore.stores.user_files, (exists) ->
 					if exists
-						grunt.log.write "OK."
+						grunt.log.writeln "OK."
 					else
 						grunt.log.error "FAIL."
 						grunt.log.errorlns """


v1.0.0-rc1

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -36,7 +36,7 @@
     exec = require("child_process").exec
     done = this.async()
     releaseVersion = grunt.template.process("<%= pkg.version %>")
-
+    console.log "git ci -am \"v#{releaseVersion}\" && git tag v#{releaseVersion}"
     child = exec "git ci -am \"v#{releaseVersion}\" && git tag v#{releaseVersion}", (error, stdout, stderr) ->
       console.log("Error running git tag: " + error) if error?
       done(!error?)


re-add browser tests

re-add browser tests

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -105,7 +105,7 @@
     if name != "grunt" and name != "grunt-cli" and (name.indexOf "grunt") == 0
       grunt.loadNpmTasks name

-  grunt.registerTask "travis", ["clean", "coffee", "copy", "browserify", "uglify"]#,  "connect", "saucelabs-jasmine"]
+  grunt.registerTask "travis", ["clean", "coffee", "copy", "browserify", "uglify", "connect", "saucelabs-jasmine"]
   grunt.registerTask "dev", ["clean", "coffee", "copy", "browserify", "uglify", "connect", "watch"]
   grunt.registerTask "default", ["dev"]


Minor cleanup.

Minor cleanup.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -95,7 +95,7 @@
     useminPrepare:
       html: [
         "<%= appConfig.dev %>/**/*.html"
-        "!<%= appConfig.dev %>/templates/**/*"
+        "!<%= appConfig.dev %>/templates/**/*.html"
       ]
       options:
         dest: "<%= appConfig.dist %>"
@@ -103,7 +103,7 @@
     usemin:
       html: [
         "<%= appConfig.dist %>/**/*.html"
-        "!<%= appConfig.dist %>/templates/**/*"
+        "!<%= appConfig.dist %>/templates/**/*.html"
       ]
       css: ["<%= appConfig.dist %>/styles/**/*.css"]
       options:
@@ -116,7 +116,7 @@
           cwd: "<%= appConfig.app %>",
           src: [
             "**/*.html"
-            "!templates/**/*"
+            "!templates/**/*.html"
           ],
           dest: "<%= appConfig.dist %>"
         ]
@@ -131,7 +131,7 @@
           src: [
             "*.{ico,txt}"
             "**/*.html"
-            "!templates/**/*"
+            "!templates/**/*.html"
             "components/**/*"
             "images/**/*.{gif,webp}"
             "styles/fonts/*"


Gruntfile: Code styling.

Gruntfile: Code styling.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -7,6 +7,7 @@
         options          :
           force          : yes
         src              : 'build'
+

     mkdir                :
       all                :
@@ -23,6 +24,7 @@
             'build/images'
           ]

+
     copy                 :
       examples           :
         expand           : yes
@@ -36,6 +38,7 @@
         src              : 'build/css/spark.css'
         dest             : 'build/docs/spark.css'

+
     coffee               :
       all                :
         options          :
@@ -66,6 +69,7 @@
         stylesheet       : 'css'
         stylesheetPath   : 'build/css/icons.css'

+
     coffee2closure       :
       all                :
         files            : [
@@ -73,6 +77,7 @@
           src            : 'build/js/**/*.js'
           ext            : '.js'
         ]
+

     stylus               :
       options            :
@@ -104,10 +109,12 @@
           ext            : '.css'
         ]

+
     templates            :
       all                :
         src              : 'src/templates/**/*.soy'
         dest             : 'build/templates/'
+

     deps                 :
       all                :
@@ -119,6 +126,7 @@
             # 'bower_components/closure-templates'
             'build'
           ]
+

     karma                :
       headless           :
@@ -163,6 +171,7 @@
           '--jscomp_error=visibility'
         ]

+
     "http-server"        :
       debug              :
         host             : '127.0.0.1'
@@ -172,6 +181,7 @@
         host             : '127.0.0.1'
         port             : 2222

+
     open                 :
       dev                :
         path             : 'http://localhost:1111/public/debug'
@@ -185,6 +195,7 @@
       build              :
         path             : 'http://localhost:2222/public/compiled'
         app              : 'Google Chrome'
+

     watch                :
       options            :
@@ -209,6 +220,7 @@
         files            : [ 'src/examples/**/*.styl', 'src/examples/**/*.coffee' ]
         tasks            : [ 'stylus:examples', 'coffee:examples', 'copy' ]

+
     jsdoc                :
       dist               :
         src              : [ 'build/js/lib/**/*.js', 'README.md', '!build/js/lib/Bootstrapper.js' ],
@@ -216,6 +228,7 @@
           destination    : 'build/docs'
           template       : 'node_modules/grunt-jsdoc/node_modules/ink-docstrap/template',
           configure      : 'jsdoc.conf.json'
+

     'gh-pages'           :
       src                : '**/*'


Only run set-development-version on mac

Only run set-development-version on mac

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -188,4 +188,8 @@
   grunt.registerTask('ci', ['update-atom-shell', 'build', 'set-development-version', 'lint', 'test'])
   grunt.registerTask('deploy', ['partial-clean', 'update-atom-shell', 'build', 'codesign'])
   grunt.registerTask('docs', ['markdown:guides', 'build-docs'])
-  grunt.registerTask('default', ['update-atom-shell', 'build', 'set-development-version', 'install'])
+
+  defaultTasks = ['update-atom-shell', 'build']
+  defaultTasks.push('set-development-version') if process.platform is 'darwin'
+  defaultTasks.push('install')
+  grunt.registerTask('default', defaultTasks)


fix grunt-replace option

fix grunt-replace option

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -43,7 +43,7 @@
       year:
         options:
           prefix: '2013 - '
-          excludePrefix: true
+          preservePrefix: true
           patterns: [
             match: "#{ new Date().getFullYear() - 1 }"
             replacement: "#{ new Date().getFullYear() }"


fix typo

fix typo

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -43,7 +43,7 @@
                     reporter: 'spec',
                     quiet: false,
                     clearRequireCache: false
-                src: ['_test/test_*.js']
+                src: ['_test/test.*.js']

     grunt.loadNpmTasks("grunt-contrib-coffee")
     grunt.loadNpmTasks("grunt-contrib-copy")


Simplified Gruntfile; added grunt-contrib-watch support; default task rebuilds Jasmine specrunners but doesn't run specs (because they fail on PhantomJS currently)

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -49,8 +49,9 @@
       outro...
     ]

-  testSources = (outro) ->
+  testSources = (intro, outro) ->
     [
+      intro...
       "common.coffee"
       "core-spec.coffee"
       "jsonp-spec.coffee"
@@ -70,7 +71,7 @@
   ]

   mynaJsTestSources   = testSources [ ], [
-    "js-init-spec.coffee"
+    # "js-init-spec.coffee"
   ]

   mynaHtmlMainSources = mainSources [ ], [
@@ -80,7 +81,8 @@
   ]

   mynaHtmlTestSources = testSources [ ], [
-    "html-init-spec.coffee"
+    "bind-spec.coffee"
+    # "html-init-spec.coffee"
   ]

   sources = (dir, sources, ext = null) ->
@@ -136,15 +138,25 @@
       mynaJs:
         src: mynaJsDistMain
         options:
-          host:       "http://127.0.0.1:#{jasminePort}/"
+          # host:       "http://127.0.0.1:#{jasminePort}/"
           specs:      sources("temp/test", mynaJsTestSources, ".js")
+          helpers:    [
+                        "lib/jquery-1.10.2.js"
+                        "temp/test/common.js"
+                      ]
+          template:   "src/test/specrunner.template.html"
           outfile:    "myna-js-specrunner.html"
           keepRunner: true
       mynaHtml:
         src: mynaHtmlDistMain
         options:
-          host:       "http://127.0.0.1:#{jasminePort}/"
+          # host:       "http://127.0.0.1:#{jasminePort}/"
           specs:      sources("temp/test", mynaHtmlTestSources, ".js")
+          helpers:    [
+                        "lib/jquery-1.10.2.js"
+                        "temp/test/common.js"
+                      ]
+          template:   "src/test/specrunner.template.html"
           outfile:    "myna-html-specrunner.html"
           keepRunner: true

@@ -156,13 +168,9 @@

     watch:
       main:
-        files: [].concat(
-          sources("src/main", mynaJsMainSources,   ".coffee")
-          sources("src/test", mynaJsTestSources,   ".coffee")
-          sources("src/main", mynaHtmlMainSources, ".coffee")
-          sources("src/test", mynaHtmlTestSources, ".coffee")
-        )
-        tasks: [ "myna-js", "myna-html" ]
+        options: livereload: true
+        files: [ "src/main/**/*", "src/test/**/*" ]
+        tasks: [ "build" ]
   })

   grunt.loadNpmTasks "grunt-contrib-coffee"
@@ -174,33 +182,23 @@
   grunt.loadNpmTasks "grunt-contrib-uglify"
   grunt.loadNpmTasks "grunt-contrib-watch"

-  grunt.registerTask "myna-js", [
+  grunt.registerTask "build", [
     "coffee"
     "copy"
-    "concat:mynaJsDist"
-    "concat:mynaJsLatest"
-    # "jshint:mynaJsMain"
-    # "jshint:mynaJsTest"
+    "concat"
+    # "jshint"
     # "connect"
-    # "jasmine:mynaJs"
-    "uglify:mynaJsDist"
-    "uglify:mynaJsLatest"
-  ]
-
-  grunt.registerTask "myna-html", [
-    "coffee"
-    "copy"
-    "concat:mynaHtmlDist"
-    "concat:mynaHtmlLatest"
-    # "jshint:mynaHtmlMain"
-    # "jshint:mynaHtmlTest"
-    # "connect"
-    # "jasmine:mynaHtml"
-    "uglify:mynaHtmlDist"
-    "uglify:mynaHtmlLatest"
+    "jasmine:mynaJs:build"
+    "jasmine:mynaHtml:build"
+    "uglify"
+  ]
+
+  grunt.registerTask "test", [
+    "build"
+    "jasmine"
   ]

   grunt.registerTask "default", [
-    "myna-js"
-    "myna-html"
-  ]
+    "build"
+    "watch"
+  ]


Correctly restart express when watching

Correctly restart express when watching

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -66,7 +66,7 @@
     watch:
       run:
         files: ['Gruntfile.coffee', 'src/**/*.coffee']
-        tasks: ['build', 'express']
+        tasks: ['build', 'express:run']
         options:
           spawn: false # Without this option specified express won't be reloaded
       test:


refactor: utilize bundle instead of bundler

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -69,10 +69,10 @@
         ]
     bgShell:
       rake:
-        cmd: 'bundler exec rake'
+        cmd: 'bundle exec rake'
         fail: true
       shotgun:
-        cmd: 'bundler exec shotgun config.ru'
+        cmd: 'bundle exec shotgun config.ru'
         bg: true
   grunt.loadNpmTasks 'grunt-html2js'
   grunt.loadNpmTasks 'grunt-contrib-sass'


Improve grunt config

Improve grunt config

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -12,7 +12,7 @@
         tasks: ['coffee', 'uglify', 'compass']

     uglify:
-      vex:
+      sortable:
         src: 'js/sortable.js'
         dest: 'js/sortable.min.js'
         options:


Lint specs too

Lint specs too

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -20,6 +20,7 @@
     # Coding standards
     coffeelint:
       components: ['components/*.coffee']
+      specs: ['spec/*.coffee']

   # Grunt plugins used for testing
   @loadNpmTasks 'grunt-contrib-watch'


Check all states

Check all states

--- client/app/Applications/Account.kdapplication/account/accSettingsPersEmailNotifications.coffee
+++ client/app/Applications/Account.kdapplication/account/accSettingsPersEmailNotifications.coffee
@@ -68,7 +68,7 @@

     toggleFieldStates = (state)->
       for flag, field of fields when flag isnt 'global'
-        if state is 'off'
+        if state in ['off', 'never']
           field.formView.hide()
         else
           field.formView.show()


revert

revert

--- scripts/populate.coffee
+++ scripts/populate.coffee
@@ -1,7 +1,6 @@
 request = require 'request-json'
-client = request.createClient 'http://weddingapi.codyseibert.com/'
+client = request.createClient 'http://localhost:8081/'
 client.headers['Authorization'] = 'admin 123456'
-return

 reservations = [
   username: 'charity'
@@ -427,4 +426,3 @@

 for reservation in reservations
   client.post 'reservations', reservation, (err, res, body) ->
-    console.log err


Added test for overriding values

Added test for overriding values

--- spec/tin-foil-object_spec.coffee
+++ spec/tin-foil-object_spec.coffee
@@ -168,7 +168,7 @@
   describe 'Extend', ->

     beforeEach ->
-      @base.prop 'extension', as: String, aliases: ['my_extension']
+      @base.prop 'extension', as: String, aliases: ['my_extension'], default_as: 'original-value'
       @extended = @base.extend()

     it 'should extend the object', ->
@@ -179,6 +179,10 @@

     it 'should still have the aliases', ->
       expect(@extended.my_extension).to.not.be.undefined
+
+    it 'should not alter base properties', ->
+      @extended.my_extension 'new-value'
+      @base.get('extension').value.should.equal 'original-value'

   describe 'Compilation', ->


:penguin: :green_heart: Humanize keystrokes in specs

--- spec/tooltip-manager-spec.coffee
+++ spec/tooltip-manager-spec.coffee
@@ -1,8 +1,12 @@
 TooltipManager = require '../src/tooltip-manager'
 {$} = require '../src/space-pen-extensions'
+_ = require "underscore-plus"

 describe "TooltipManager", ->
   [manager, element] = []
+
+  ctrlX = _.humanizeKeystroke("ctrl-x")
+  ctrlY = _.humanizeKeystroke("ctrl-y")

   beforeEach ->
     manager = new TooltipManager
@@ -35,7 +39,7 @@

           hover element, ->
             tooltipElement = document.body.querySelector(".tooltip")
-            expect(tooltipElement).toHaveText "Title ⌃X ⌃Y"
+            expect(tooltipElement).toHaveText "Title #{ctrlX} #{ctrlY}"

       describe "when no title is specified", ->
         it "shows the key binding corresponding to the command alone", ->
@@ -45,7 +49,7 @@

           hover element, ->
             tooltipElement = document.body.querySelector(".tooltip")
-            expect(tooltipElement).toHaveText "⌃X ⌃Y"
+            expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}"

       describe "when a keyBindingTarget is specified", ->
         it "looks up the key binding relative to the target", ->
@@ -57,7 +61,7 @@

           hover element, ->
             tooltipElement = document.body.querySelector(".tooltip")
-            expect(tooltipElement).toHaveText "⌃X ⌃Y"
+            expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}"

         it "does not display the keybinding if there is nothing mapped to the specified keyBindingCommand", ->
           manager.add element, title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element


Made karma happier

Made karma happier

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -21,12 +21,12 @@

 angular.module('app', ['controllers', 'services', 'directives'])

-  .config ['$httpProvider', ($httpProvider) ->
+  .config(['$httpProvider', ($httpProvider) ->
     $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content')
-  ]
+  ])

-  .config ['$routeProvider', ($routeProvider) ->
+  .config(['$routeProvider', ($routeProvider) ->
     $routeProvider
       .when('/:id',{templateUrl:'slide.html',controller:'SlideController'})
       .otherwise({redirectTo:'/0'})
-  ]
+  ])


Be consistent on naming.

Be consistent on naming.

It was named as $emojiBtn before I was using $emojiButton so updated them to be consistent.

--- app/assets/javascripts/awards_handler.coffee
+++ app/assets/javascripts/awards_handler.coffee
@@ -115,18 +115,18 @@
     @checkMutuality votesBlock, emoji  if checkForMutuality
     @addEmojiToFrequentlyUsedList emoji

-    emoji     = @normilizeEmojiName emoji
-    $emojiBtn = @findEmojiIcon(votesBlock, emoji).parent()
-
-    if $emojiBtn.length > 0
-      if @isActive $emojiBtn
-        @decrementCounter $emojiBtn, emoji
+    emoji        = @normilizeEmojiName emoji
+    $emojiButton = @findEmojiIcon(votesBlock, emoji).parent()
+
+    if $emojiButton.length > 0
+      if @isActive $emojiButton
+        @decrementCounter $emojiButton, emoji
       else
-        counter = $emojiBtn.find '.js-counter'
+        counter = $emojiButton.find '.js-counter'
         counter.text parseInt(counter.text()) + 1
-        $emojiBtn.addClass 'active'
+        $emojiButton.addClass 'active'
         @addMeToUserList votesBlock, emoji
-        @animateEmoji $emojiBtn
+        @animateEmoji $emojiButton
     else
       votesBlock.removeClass 'hidden'
       @createEmoji votesBlock, emoji
@@ -153,32 +153,32 @@
       @addAward votesBlock, awardUrl, mutualVote, no if isAlreadyVoted

-  isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active'
-
-
-  decrementCounter: ($emojiBtn, emoji) ->
-
-    counter       = $('.js-counter', $emojiBtn)
+  isActive: ($emojiButton) -> $emojiButton.hasClass 'active'
+
+
+  decrementCounter: ($emojiButton, emoji) ->
+
+    counter       = $('.js-counter', $emojiButton)
     counterNumber = parseInt counter.text(), 10

     if counterNumber > 1
       counter.text counterNumber - 1
-      @removeMeFromUserList $emojiBtn, emoji
+      @removeMeFromUserList $emojiButton, emoji
     else if emoji is 'thumbsup' or emoji is 'thumbsdown'
-      $emojiBtn.tooltip 'destroy'
+      $emojiButton.tooltip 'destroy'
       counter.text '0'
-      @removeMeFromUserList $emojiBtn, emoji
-      @removeEmoji $emojiBtn if $emojiBtn.parents('.note').length
-    else
-      @removeEmoji $emojiBtn
-
-    $emojiBtn.removeClass 'active'
-
-
-  removeEmoji: ($emojiBtn) ->
-
-    $emojiBtn.tooltip('destroy')
-    $emojiBtn.remove()
+      @removeMeFromUserList $emojiButton, emoji
+      @removeEmoji $emojiButton if $emojiButton.parents('.note').length
+    else
+      @removeEmoji $emojiButton
+
+    $emojiButton.removeClass 'active'
+
+
+  removeEmoji: ($emojiButton) ->
+
+    $emojiButton.tooltip('destroy')
+    $emojiButton.remove()

     $votesBlock = @getVotesBlock()

@@ -191,9 +191,9 @@
     return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title')

-  removeMeFromUserList: ($emojiBtn, emoji) ->
-
-    awardBlock    = $emojiBtn
+  removeMeFromUserList: ($emojiButton, emoji) ->
+
+    awardBlock    = $emojiButton
     originalTitle = @getAwardTooltip awardBlock

     authors = originalTitle.split ', '


fix(quickschedule): properly handle errors from QuickSchedule server

--- examples/N1-Quick-Schedule/lib/availability-composer-extension.coffee
+++ examples/N1-Quick-Schedule/lib/availability-composer-extension.coffee
@@ -21,6 +21,8 @@
       serverUrl = "https://quickschedule.herokuapp.com/register-events"
       post({url: serverUrl, body: JSON.stringify(data)})
       .then (args) =>
+        if args[0].statusCode != 200
+          throw new Error()
         data = args[1]
         return data
       .catch (error) ->


TODO add

TODO add

--- js/token-poker-hubot-dealer.coffee
+++ js/token-poker-hubot-dealer.coffee
@@ -155,6 +155,8 @@
     '' + msg.message.user.room

   currentDealer = (msg) ->
+    # TODO - DO NOT THROW ON THIS IF NOT ENABLED, JUST ROLL ON AND HAVE ALL COMMANDS IGNORE
+    # esp. a problem with convenient command regexs that don't require addressing hubot
     rooms.getDealerForRoom(currentRoom(msg))

   dealerFactory = (room) ->


fux bug

fux bug

--- app/assets/javascripts/s3_form_for.js.coffee
+++ app/assets/javascripts/s3_form_for.js.coffee
@@ -68,8 +68,7 @@

       loadImage.parseMetaData file, (img) ->
         ornt = if img.exif? then img.exif.get("Orientation") else 1
-
-        image_contaner = $(".s3_image")
+        image_contaner = $($uploadForm).find(".s3_image")
         attr_image_max_height = image_contaner.data('image-max-height')
         max_height =  if attr_image_max_height == undefined
                         48
@@ -106,14 +105,15 @@
             $(item).next().remove()
           $(item).attr('disabled','disabled')

-      if settings.allow_send_form_without_file && $uploadForm.find('#file_name_for_upload').text().length == 0
+      file_name_for_upload_text = $uploadForm.find('#file_name_for_upload').text()
+      if settings.allow_send_form_without_file && file_name_for_upload_text == 'No file selected'
         $.each $uploadForm.find(settings.disable_fields_after_submit), (index, item) =>
           $(item).removeAttr('disabled')

       if  validation_form
         $(forms_for_submit).submit()

-      if settings.allow_send_form_without_file && $uploadForm.find('#file_name_for_upload').text() == 'No file selected'
+      if settings.allow_send_form_without_file && file_name_for_upload_text == 'No file selected'
         true
       else
         false


fix dropdown menu

fix dropdown menu

--- app/assets/javascripts/active_admin/lib/dropdown-menu.js.coffee
+++ app/assets/javascripts/active_admin/lib/dropdown-menu.js.coffee
@@ -9,7 +9,7 @@
       onClickActionItemCallback: null
     }

-    @options = $.extend defaults, options
+    @options = $.extend defaults, @options
     @isOpen  = false

     @$menuButton = @$element.find '.dropdown_menu_button'


[#4] Adiciona index da UO no gráfico de distribuição por UOs

--- app/assets/javascripts/angular_app/directives/uoGraph.js.coffee
+++ app/assets/javascripts/angular_app/directives/uoGraph.js.coffee
@@ -1,5 +1,6 @@
 angular.module('InescApp').directive 'uoGraph', ['$filter', ($filter) ->
   columns = [
+    { sTitle: '#', bSortable: false }
     { sTitle: 'Entidade Orçamentária', bSortable: false }
     { sTitle: 'Orçamento Autorizado', bSortable: false, sClass: 'currency' }
     { sTitle: 'Percentual do total', bSortable: false, sClass: 'percentual' }
@@ -10,16 +11,20 @@
     bPaginate: false
     aaSorting: [[ 3, 'desc' ]]
     sDom: 'ft'
+    fnRowCallback: (nRow, aData, iDisplayIndex) ->
+      $('td:eq(0)', nRow).html(iDisplayIndex + 1)
+      nRow

   processData = (entity, year) ->
     entityUrl = $filter('entityUrl')
     currency = $filter('currency')
     percentual = $filter('percentual')
     entity.unidades_orcamentarias.map (uo) ->
-      ["#{uo.label}"
-        currency(uo.amount, '')
-        percentual((uo.amount*100)/entity.autorizado.total)
-        uo.amount]
+      [""
+       "#{uo.label}"
+       currency(uo.amount, '')
+       percentual((uo.amount*100)/entity.autorizado.total)
+       uo.amount]

   restrict: 'E',
   template: '<my-data-table columns="columns" options="options" data="data"></my-data-table>',


only update favicon on the build page

only update favicon on the build page

--- js/app.coffee
+++ js/app.coffee
@@ -71,9 +71,12 @@
     @favicon_updator = @komp noop

     @selected.subscribe (selected) =>
+      @favicon_updator.dispose()
+
       if selected.favicon_updator?
-        @favicon_updator.dispose()
         @favicon_updator = @komp selected.favicon_updator
+      else
+        @reset_favicon()

     # outer
     @home = new CI.outer.Home("home", "Continuous Integration and Deployment")
@@ -88,13 +91,15 @@
     @query_results_query = ko.observable(null)
     @query_results = ko.observableArray([])

+  set_favicon_color: (color) =>
+    $("link[rel='icon']").attr('href', "/favicon-#{color}.png?v=1")
+
   build_favicon_updator: (build) =>
     if build?
-      $("link[rel='icon']").attr('href', "/favicon-#{build.favicon_color()}.png?v=1")
-
-  build_array_favicon_updator: (observable_array) =>
-    if observable_array().length
-      @build_favicon_updator(observable_array()[0])
+      @set_favicon_color(build.favicon_color())
+
+  reset_favicon: () =>
+    @set_favicon_color() # undefined resets it

   cleanObjs: (objs) ->
     $.each objs, (i, o) ->
@@ -426,8 +431,6 @@
   @get '^/', (cx) =>
     VM.selected
       refresh_fn: VM.loadRecentBuilds
-      favicon_updator: =>
-        VM.build_array_favicon_updator(VM.recent_builds)

     VM.loadRootPage(cx)

@@ -468,8 +471,6 @@
         branch: branch
         refresh_fn: =>
           VM.loadProject(cx.params.username, cx.params.project, branch, true)
-        favicon_updator: =>
-          VM.build_array_favicon_updator(VM.builds)

       if project_name is VM.selected().project_name and branch is VM.selected().branch
         sel = _.extend(VM.selected(), sel)
@@ -508,8 +509,6 @@
         project_name: "#{cx.params.username}/#{cx.params.project}"
         refresh_fn: =>
           VM.loadProject(cx.params.username, cx.params.project, null, true)
-        favicon_updator: =>
-          VM.build_array_favicon_updator(VM.builds)

       if project_name is VM.selected().project_name
         sel = _.extend(VM.selected(), sel)


sync sprite anum

sync sprite anum

--- src/Ball.coffee
+++ src/Ball.coffee
@@ -44,9 +44,6 @@
   getTimeSinceKick:->
     Date.now() - @kickTime

-  getSpeed:->
-    @sprite.body.velocity.x**2 + @sprite.body.velocity.y**2
-
   setState:(state)->
     if !@spawned
       @sprite.position.x = state.x
@@ -61,12 +58,16 @@
       blend.to({ x: state.x, y: state.y + 6 }, @rate, Phaser.Easing.Linear.None, true, 0, 0)
     @possessorId = state.possessorId
     @catchable = state.catchable
+    if @sprite.animations.currentAnim? && @sprite.animations.currentAnim.name != state.anim
+      @sprite.animations.play(state.anim)

   getState:(state)->
     x: @sprite.position.x,
     y: @sprite.position.y,
     possessorId: @possessorId
     catchable: @catchable
+    anim: @sprite.animations.currentAnim.name
+    angular: @sprite.body.angularVelocity

   remove: ->
     @sprite.kill()


add 'Close' button at bottom of dialog

add 'Close' button at bottom of dialog

--- client/driver_lines.coffee
+++ client/driver_lines.coffee
@@ -140,6 +140,9 @@
     window.dialog_template = bootbox.dialog
       message: window.renderTmp(Template.AddDriverLineDialog)
       buttons:
+        close:
+          label: "Close"
+          className: "btn-default"
         save:
           label: "Save"
           className: "btn-primary"


Fixed a bug where remove would return a function rather than the actual list. This was because I was accessing the tail property, not invoking the function.

--- src/List.coffee
+++ src/List.coffee
@@ -122,7 +122,7 @@
     # can remove the following if/else with this.tail().getOrElse(new List()) once
     # tail and head are functions that return an option instead.
     if !this.tail().isEmpty()
-      this.cons(this.tail().first().get(), this.tail().tail)
+      this.cons(this.tail().first().get(), this.tail().tail())
     else
       new mugs.List()
   else


Logging for debug removed

Logging for debug removed

--- src/Moon.coffee
+++ src/Moon.coffee
@@ -161,8 +161,6 @@
                 auxDelay = totalTime - timeDiff - anm.duration
                 auxDuration = totalTime - timeDiff

-                console.log (auxDelay + " | " + auxDuration)
-
                 if auxDelay < 0
                     anm.delay = 0
                 else
@@ -170,8 +168,6 @@

                 if auxDuration < anm.duration
                     anm.duration = auxDuration
-
-                console.log anm.delay + " | " + anm.duration

                 this._paused = null


Add atom.inSpecMode()

Add atom.inSpecMode()

--- src/atom.coffee
+++ src/atom.coffee
@@ -220,6 +220,9 @@
   inDevMode: ->
     @getLoadSettings().devMode

+  inSpecMode: ->
+    @getLoadSettings().isSpec
+
   toggleFullScreen: ->
     @setFullScreen(!@isFullScreen())


Sorting

Sorting

--- src/base.coffee
+++ src/base.coffee
@@ -309,6 +309,57 @@
         x[0].city.should.eql 'sthlm'
       ).then ->
         api.close(done)
+
+
+
+    it "can sort objects", (done) ->
+      api = manikin.create()
+
+      model =
+        stuffz:
+          fields:
+            name: 'string'
+            age: 'number'
+
+      saved = {}
+
+      promise(api).connect(connectionData, model, noErr())
+      .post('stuffz', { name: 'jakob', age: 28 }, noErr())
+      .post('stuffz', { name: 'jakob', age: 42 }, noErr())
+      .post('stuffz', { name: 'julia', age: 27 }, noErr())
+      .list('stuffz', { sort: { age: 'asc' } }, noErr (x) ->
+        x.should.have.length 3
+        x[0].age.should.eql 27
+        x[1].age.should.eql 28
+        x[2].age.should.eql 42
+      ).then ->
+        api.close(done)
+
+
+
+    it "can sort objects in a descending", (done) ->
+      api = manikin.create()
+
+      model =
+        stuffz:
+          fields:
+            name: 'string'
+            age: 'number'
+
+      saved = {}
+
+      promise(api).connect(connectionData, model, noErr())
+      .post('stuffz', { name: 'jakob', age: 28 }, noErr())
+      .post('stuffz', { name: 'jakob', age: 42 }, noErr())
+      .post('stuffz', { name: 'julia', age: 27 }, noErr())
+      .list('stuffz', { sort: { age: 'desc' } }, noErr (x) ->
+        x.should.have.length 3
+        x[0].age.should.eql 42
+        x[1].age.should.eql 28
+        x[2].age.should.eql 27
+      ).then ->
+        api.close(done)
+

     it "can get a single object from its ID", (done) ->


Client: Sidebar: Added hidden-notifications class to show popup correct positions

--- client/app/lib/avatararea/accountpopup.coffee
+++ client/app/lib/avatararea/accountpopup.coffee
@@ -8,7 +8,8 @@

   constructor: (options = {}, data) ->

-    options.cssClass = kd.utils.curry 'account', options.cssClass
+    defaultClasses   = if isSoloProductLite() then 'account hidden-notifications' else 'account'
+    options.cssClass = kd.utils.curry defaultClasses, options.cssClass

     super options, data


Define get settings method

Define get settings method

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -1094,3 +1094,10 @@

     for own key, value of @initialSettings
       @settings.set key, value
+
+
+  getSettings: ->
+
+    rval = {}
+    rval[key] = value  for [key, value] in @settings.items()
+    return rval


FinderController: respect the hideDotFiles option if its provided, update Framework

--- client/finder/filetree/controllers/findercontroller.coffee
+++ client/finder/filetree/controllers/findercontroller.coffee
@@ -253,6 +253,7 @@
         , yes

   isNodesHiddenFor:(vmName)->
+    return yes  if @getOption 'hideDotFiles'
     pipedVm = @_pipedVmName vmName
     return (@appStorage.getValue('vmsDotFileChoices') or {})[pipedVm]


cleanup examples - jsonify

cleanup examples - jsonify

--- content/pages/_components/ide/content/preview/index.coffee
+++ content/pages/_components/ide/content/preview/index.coffee
@@ -48,11 +48,13 @@
     @set "logs", logs = new bindable.Collection()
     ["log", "error", "warn", "notice"].forEach (level) ->
       _console[level] = () ->
-        if typeof text is "object"
-          text = JSON.stringify text, null, 2
         logs.push new bindable.Object {
           level: level,
-          text: Array.prototype.slice.call(arguments).join(" ")
+          text: Array.prototype.slice.call(arguments).map((v) ->
+            if typeof v is "object"
+              v = JSON.stringify v, null, 2
+            return v
+          ).join(" ")
         }


Increase description size

Increase description size

--- server/bootstrap.coffee
+++ server/bootstrap.coffee
@@ -15,7 +15,7 @@
 					_id: Users.findOne({'emails.address': 'test@euajudo.io'})._id
 					name: Users.findOne({'emails.address': 'test@euajudo.io'}).profile.name
 				createdAt: new Date
-				description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, voluptatem, et. Dolor corporis tenetur nihil sit, magnam exercitationem, assumenda molestias pariatur soluta ullam maxime repellendus alias eaque impedit recusandae. Magni.'
+				description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, voluptatem, et. Dolor corporis tenetur nihil sit, magnam exercitationem, assumenda molestias pariatur soluta ullam maxime repellendus alias eaque impedit recusandae. Magni. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, voluptatem, et. Dolor corporis tenetur nihil sit, magnam exercitationem, assumenda molestias pariatur soluta ullam maxime repellendus alias eaque impedit recusandae. Magni. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, voluptatem, et. Dolor corporis tenetur nihil sit, magnam exercitationem, assumenda molestias pariatur soluta ullam maxime repellendus alias eaque impedit recusandae. Magni. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, voluptatem, et. Dolor corporis tenetur nihil sit, magnam exercitationem, assumenda molestias pariatur soluta ullam maxime repellendus alias eaque impedit recusandae. Magni. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, voluptatem, et. Dolor corporis tenetur nihil sit, magnam exercitationem, assumenda molestias pariatur soluta ullam maxime repellendus alias eaque impedit recusandae. Magni.'


Compile nu.nl javascript and css

Compile nu.nl javascript and css

--- js-library/Gruntfile.coffee
+++ js-library/Gruntfile.coffee
@@ -69,10 +69,14 @@
           'build/js/loader/loader_bookmarklet.js'
         ]
         dest: 'build/factlink_loader_bookmarklet.js'
+      publisher:
+        src:  'build/js/publisher_poc/nu.js'
+        dest: 'build/nu.js'
     less:
       build:
         files:
           'build/css/basic.css': 'app/css/basic.less'
+          'build/css/publisher_poc/nu.nl.css': 'app/css/publisher_poc/nu.nl.less'
     cssmin:
       build:
         options:


1/2 way through converting to using Promises

--- src/art/engine/file/v1_loader.coffee
+++ src/art/engine/file/v1_loader.coffee
@@ -68,18 +68,17 @@
   @load: (data, bitmapFactory) -> @singleton.load data, bitmapFactory

   load: (data, @bitmapFactory = Canvas.Bitmap) ->
-    new Promise (resolve) =>
-      xbd = Xbd.parse data
-      log "V1Loader.load": xbd
-      topTag = xbd.tag("art_file")
-      @decodeTopTag topTag, (artFile) =>
-        artFile.axis = point()
-        artFile.location = point()
-
-        artFile.children = (child for child in artFile.getPendingChildren() when !child.getPendingIsMask())
-        artFile.bitmapFactory = @bitmapFactory
-        stateEpoch.onNextReady ->
-          resolve artFile
+    xbd = Xbd.parse data
+    log "V1Loader.load": xbd
+    topTag = xbd.tag("art_file")
+    @decodeTopTag topTag
+    .then (artFile) =>
+      artFile.axis = point()
+      artFile.location = point()
+
+      artFile.children = (child for child in artFile.getPendingChildren() when !child.getPendingIsMask())
+      artFile.bitmapFactory = @bitmapFactory
+      stateEpoch.onNextReady -> artFile

   @objectFactory =
     art_file: -> new Model
@@ -106,39 +105,28 @@
         new Canvas.Bitmap decodedBitmap
       @decodeBitmaps bitmaps, keys, index+1, callBack

-  decodeBitmapsTag: (bitmapsTag, callBack) ->
-    log "decodeBitmapsTag 1", bitmapsTag
-    bitmaps = {}
-    for tag in bitmapsTag.tags
-      id = tag.attrs["bitmap_id"] | 0
-      bitmapData = tag.attrs["pixel_data"]
-      bitmaps[id] = bitmapData
-
-    log "decodeBitmapsTag 2"
-    @decodeBitmaps bitmaps, Object.keys(bitmaps), 0, callBack
-
-  decodeContext: (topTag, callBack) ->
-    log "decodeContext 1"
-    bitmapsTag = topTag.tag "bitmaps"
-
-    postDecodeBitmaps = (bitmaps)=>
-      log "decodeContext 2"
-      @bitmaps = bitmaps
-      callBack()
-
-    log "decodeContext 3"
-    if bitmapsTag
-      @decodeBitmapsTag bitmapsTag, postDecodeBitmaps
-    else
-      postDecodeBitmaps null
-
-  decodeTopTag: (topTag, callBack) ->
-    log "decodeTopTag 1"
-    @decodeContext topTag, =>
-      log "decodeTopTag 2"
-      topElement = @createElementFromTag topTag
-      callBack topElement
-
+  decodeBitmapsTag: (bitmapsTag) ->
+    new Promise (resolve) =>
+      log "decodeBitmapsTag 1", bitmapsTag
+      bitmaps = {}
+      for tag in bitmapsTag.tags
+        id = tag.attrs["bitmap_id"] | 0
+        bitmapData = tag.attrs["pixel_data"]
+        bitmaps[id] = bitmapData
+
+      log "decodeBitmapsTag 2"
+      @decodeBitmaps bitmaps, Object.keys(bitmaps), 0, resolve
+
+  decodeContext: (topTag) ->
+    Promise.resolve()
+    .then =>
+      if bitmapsTag = topTag.tag "bitmaps"
+        @decodeBitmapsTag bitmapsTag
+    .then (@bitmaps) => @bitmaps
+
+  decodeTopTag: (topTag) ->
+    @decodeContext topTag
+    .then => @createElementFromTag topTag

   createElement: (tag) ->
     constructor = V1Loader.objectFactory[tag.name]