Reorganize native / trapped methods in a more readable form.

--- src/methods.coffee
+++ src/methods.coffee
@@ -57,44 +57,78 @@
     if @access_flags.static
       @static_value = null  # loaded in when getstatic is called

-trapped_methods = {
-  'java/lang/System::setJavaLangAccess()V': (rs) -> #NOP
-  'java/lang/System::loadLibrary(Ljava/lang/String;)V': (rs) ->
-      args = rs.curr_frame().locals
-      lib = rs.jvm2js_str rs.get_obj args[0]
-      error "Attempt to load library '#{lib}' failed: library loads are NYI"
-  'java/lang/System::adjustPropertiesForBackwardCompatibility(Ljava/util/Properties;)V': (rs) -> #NOP (apple-java specific?)
-  'java/lang/Terminator::setup()V': (rs) -> #NOP
-  'java/util/concurrent/atomic/AtomicInteger::<clinit>()V': (rs) -> #NOP
-  'java/util/concurrent/atomic/AtomicInteger::compareAndSet(II)Z': (rs) -> rs.push 1  # always true
-  'sun/misc/Unsafe::getUnsafe()Lsun/misc/Unsafe;': ((rs) -> # avoid reflection
-    rs.static_get({'class':'sun/misc/Unsafe','sig':{'name':'theUnsafe'}}))
-  'java/util/concurrent/atomic/AtomicReferenceFieldUpdater::newUpdater(Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/String;)Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater;': (rs) -> rs.push 0 # null
-  'java/nio/charset/Charset$3::run()Ljava/lang/Object;': (rs) -> rs.push 0 # null
-  'java/nio/Bits::byteOrder()Ljava/nio/ByteOrder;': (rs) -> rs.static_get {'class':'java/nio/ByteOrder','sig':{'name':'LITTLE_ENDIAN'}}
-  'java/lang/Class::newInstance0()Ljava/lang/Object;': ((rs) -> #implemented here to avoid reflection
-    classname = rs.get_obj(rs.curr_frame().locals[0]).obj.name
-    rs.push (oref = rs.init_object(classname))
-    rs.method_lookup({'class':classname,'sig':{'name':'<init>'}}).run(rs)
-    rs.push oref
-    )
-  'java/io/PrintStream::write(Ljava/lang/String;)V': ((rs) ->
-    args = rs.curr_frame().locals
-    str = rs.jvm2js_str(rs.get_obj(args[1]))
-    rs.static_get {'class':'java/lang/System','sig':{'name':'out'}}; sysout = rs.pop()
-    rs.static_get {'class':'java/lang/System','sig':{'name':'err'}}; syserr = rs.pop()
-    if args[0] is sysout
-      rs.print str
-    else if args[0] is syserr
-      rs.print str
-    else
-      throw "You tried to write to a PrintStream that wasn't System.out or System.err! For shame!"
-    )
-  'java/lang/ref/Reference::<clinit>()V': (rs) -> #NOP
-  'java/util/concurrent/locks/AbstractQueuedSynchronizer::<clinit>()V': (rs) -> #NOP
-  'java/util/concurrent/locks/AbstractQueuedSynchronizer::compareAndSetState(II)Z': (rs) -> rs.push 1 # always true
-  'java/util/concurrent/locks/AbstractQueuedSynchronizer::release(I)Z': (rs) -> rs.push 1 # always true
-}
+# convenience function. idea taken from coffeescript's grammar
+o = (fn_name, fn) -> fn_name: fn_name, fn: fn
+
+trapped_methods =
+  java:
+    lang:
+      Class: [
+        o 'newInstance0()Ljava/lang/Object;', (rs) -> #implemented here to avoid reflection
+            classname = rs.get_obj(rs.curr_frame().locals[0]).obj.name
+            rs.push (oref = rs.init_object(classname))
+            rs.method_lookup({'class':classname,'sig':{'name':'<init>'}}).run(rs)
+            rs.push oref
+      ]
+      ref:
+        Reference: [
+          o '<clinit>()V', (rs) -> #NOP
+        ]
+      System: [
+        o 'setJavaLangAccess()V', (rs) -> # NOP
+        o 'loadLibrary(Ljava/lang/String;)V', (rs) ->
+            args = rs.curr_frame().locals
+            lib = rs.jvm2js_str rs.get_obj args[0]
+            error "Attempt to load library '#{lib}' failed: library loads are NYI"
+        o 'adjustPropertiesForBackwardCompatibility(Ljava/util/Properties;)V', (rs) -> #NOP (apple-java specific?)
+      ]
+      Terminator: [
+        o 'setup()V', (rs) -> #NOP
+      ]
+    util:
+      concurrent:
+        atomic:
+          AtomicInteger: [
+            o '<clinit>()V', (rs) -> #NOP
+            o 'compareAndSet(II)Z', (rs) -> rs.push 1  # always true
+          ]
+          AtomicReferenceFieldUpdater: [
+            o 'newUpdater(Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/String;)Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater;', (rs) -> rs.push 0 # null
+          ]
+        locks:
+          AbstractQueuedSynchronizer: [
+            o '<clinit>()V', (rs) -> #NOP
+            o 'compareAndSetState(II)Z', (rs) -> rs.push 1 # always true
+            o 'release(I)Z', (rs) -> rs.push 1 # always true
+          ]
+    nio:
+      charset:
+        Charset$3: [
+          o 'run()Ljava/lang/Object;', (rs) -> rs.push 0 # null
+        ]
+      Bits: [
+        o 'byteOrder()Ljava/nio/ByteOrder;', (rs) -> rs.static_get {'class':'java/nio/ByteOrder','sig':{'name':'LITTLE_ENDIAN'}}
+      ]
+    io:
+      PrintStream: [
+        o 'write(Ljava/lang/String;)V', (rs) ->
+            args = rs.curr_frame().locals
+            str = rs.jvm2js_str(rs.get_obj(args[1]))
+            rs.static_get {'class':'java/lang/System','sig':{'name':'out'}}; sysout = rs.pop()
+            rs.static_get {'class':'java/lang/System','sig':{'name':'err'}}; syserr = rs.pop()
+            if args[0] is sysout
+              rs.print str
+            else if args[0] is syserr
+              rs.print str
+            else
+              throw "You tried to write to a PrintStream that wasn't System.out or System.err! For shame!"
+      ]
+  sun:
+    misc:
+      Unsafe: [
+        o 'getUnsafe()Lsun/misc/Unsafe;', (rs) -> # avoid reflection
+            rs.static_get({'class':'sun/misc/Unsafe','sig':{'name':'theUnsafe'}})
+      ]

 doPrivileged = (rs) ->
   oref = rs.curr_frame().locals[0]
@@ -103,118 +137,161 @@
   rs.push oref unless m.access_flags.static
   m.run(rs,m.access_flags.virtual)

-native_methods = {
-  'java/lang/System::arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V': ((rs) ->
-    args = rs.curr_frame().locals
-    src_array = rs.get_obj(args[0]).obj.array
-    src_pos = args[1]
-    dest_array = rs.get_obj(args[2]).obj.array
-    dest_pos = args[3]
-    length = args[4]
-    j = dest_pos
-    for i in [src_pos...src_pos+length]
-      dest_array[j++] = src_array[i]
-    )
-  'java/lang/Float::floatToRawIntBits(F)I': ((rs) ->  #note: not tested for weird values
-    f_val = rs.curr_frame().locals[0]
-    sign = if f_val < 0 then 1 else 0
-    f_val = Math.abs(f_val)
-    exp = Math.floor(Math.log(f_val)/Math.LN2)
-    sig = (f_val/Math.pow(2,exp)-1)/Math.pow(2,-23)
-    rs.push (sign<<31)+((exp+127)<<23)+sig
-    )
-  'java/lang/Double::doubleToRawLongBits(D)J': ((rs) ->#note: not tested at all
-    d_val = rs.curr_frame().locals[0]
-    sign = if d_val < 0 then 1 else 0
-    d_val = Math.abs(d_val)
-    exp = Math.floor(Math.log(d_val)/Math.LN2)
-    sig = (d_val/Math.pow(2,exp)-1)/Math.pow(2,-52)
-    rs.push util.lshift(sign,63)+util.lshift(exp+1023,52)+sig, null
-    )
-  'java/security/AccessController::doPrivileged(Ljava/security/PrivilegedAction;)Ljava/lang/Object;': doPrivileged
-  'java/security/AccessController::doPrivileged(Ljava/security/PrivilegedExceptionAction;)Ljava/lang/Object;': doPrivileged
-  'java/io/FileSystem::getFileSystem()Ljava/io/FileSystem;': (rs) -> rs.heap_new('java/io/UnixFileSystem')
-  'java/lang/StrictMath::pow(DD)D': (rs) -> rs.push Math.pow(rs.cl(0),rs.cl(2)), null
-  'sun/misc/VM::initialize()V': (rs) ->  # NOP???
-  'sun/reflect/Reflection::getCallerClass(I)Ljava/lang/Class;': ((rs) ->
-    frames_to_skip = rs.curr_frame().locals[0]
-    #TODO: disregard frames assoc. with java.lang.reflect.Method.invoke() and its implementation
-    cls = rs.meta_stack[rs.meta_stack.length-1-frames_to_skip].class_name
-    rs.push rs.set_obj 'java/lang/Class', { name:cls }
-    )
-  'java/lang/System::currentTimeMillis()J': (rs) -> rs.push (new Date).getTime(), null
-  'java/lang/String::intern()Ljava/lang/String;': ((rs) ->
-    str_ref = rs.curr_frame().locals[0]
-    js_str = rs.jvm2js_str(rs.get_obj(str_ref))
-    unless rs.string_pool[js_str]
-      rs.string_pool[js_str] = str_ref
-    rs.push rs.string_pool[js_str]
-    )
-  'java/lang/Class::getPrimitiveClass(Ljava/lang/String;)Ljava/lang/Class;': ((rs) ->
-    str = rs.get_obj(rs.curr_frame().locals[0])
-    carr = rs.get_obj(str.obj.value).obj.array
-    name = (String.fromCharCode(c) for c in carr).join('') # XXX convert to unicode
-    rs.push rs.set_obj 'java/lang/Class', { name: name }
-    )
-  'java/lang/Thread::currentThread()Ljava/lang/Thread;': (rs) -> rs.push rs.set_obj 'java/lang/Thread' # mock thread
-  'java/lang/Object::getClass()Ljava/lang/Class;': (rs) ->
-    _this = rs.get_obj(rs.curr_frame().locals[0])
-    rs.push rs.set_obj 'java/lang/Class', { name:util.ext_classname _this.type}
-  'java/lang/Class::getClassLoader0()Ljava/lang/ClassLoader;': (rs) -> rs.push 0  # we don't need no stinkin classloaders
-  'java/lang/Class::desiredAssertionStatus0(Ljava/lang/Class;)Z': (rs) -> rs.push 0 # we don't need no stinkin asserts
-  'java/lang/Class::getName0()Ljava/lang/String;': (rs) -> rs.push rs.init_string(rs.get_obj(rs.curr_frame().locals[0]).obj.name)
-  'java/lang/Class::forName0(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;': ((rs) ->
-    jvm_str = rs.get_obj(rs.curr_frame().locals[0])
-    classname = rs.jvm2js_str(jvm_str).replace(/\./g,'/')
-    throw "Class.forName0: Failed to load #{classname}" unless rs.class_lookup(classname)
-    rs.push rs.set_obj 'java/lang/Class', { name:classname }
-    )
-  'java/lang/System::initProperties(Ljava/util/Properties;)Ljava/util/Properties;': ((rs) ->
-    p_ref = rs.curr_frame().locals[0]
-    m = rs.method_lookup({'class':'java/util/Properties','sig':{'name':'setProperty'}})
-    # properties to set:
-    #  java.version,java.vendor,java.vendor.url,java.home,java.class.version,java.class.path,
-    #  os.name,os.arch,os.version,file.separator,path.separator,
-    #  user.name,user.home,user.dir
-    props = {'file.encoding':'US_ASCII','java.vendor':'Coffee-JVM','line.separator':'\n'}
-    for k,v of props
-      rs.push p_ref, rs.init_string(k,true), rs.init_string(v,true)
-      m.run(rs)
-      rs.pop()  # we don't care about the return value
-    rs.push p_ref
-    )
-  'java/lang/Throwable::fillInStackTrace()Ljava/lang/Throwable;': (rs) ->
-    #TODO possibly filter out the java calls from our own call stack.
-    # at the moment, this is effectively a NOP.
-    rs.push rs.curr_frame().locals[0]
-  'java/lang/System::setIn0(Ljava/io/InputStream;)V': ((rs) ->
-    rs.push rs.curr_frame().locals[0] # move oref to the stack for static_put
-    rs.static_put {'class':'java/lang/System','sig':{'name':'in'}}
-    )
-  'java/lang/System::setOut0(Ljava/io/PrintStream;)V': ((rs) ->
-    rs.push rs.curr_frame().locals[0] # move oref to the stack for static_put
-    rs.static_put {'class':'java/lang/System','sig':{'name':'out'}}
-    )
-  'java/lang/System::setErr0(Ljava/io/PrintStream;)V': ((rs) ->
-    rs.push rs.curr_frame().locals[0] # move oref to the stack for static_put
-    rs.static_put {'class':'java/lang/System','sig':{'name':'err'}}
-    )
-  'java/lang/Class::getComponentType()Ljava/lang/Class;': (rs) ->
-    type = rs.get_obj(rs.curr_frame().locals[0]).obj.name
-    component_type = /\[+(.*)/.exec(type)[1]
-    rs.push rs.set_obj 'java/lang/Class', name:component_type
-  'java/lang/reflect/Array::newArray(Ljava/lang/Class;I)Ljava/lang/Object;': (rs) ->
-    type = rs.get_obj(rs.curr_frame().locals[0]).obj.name
-    len = rs.curr_frame().locals[0]
-    rs.heap_newarray util.int_classname type, len
-  'java/io/FileOutputStream::writeBytes([BII)V': (rs) ->
-    args = rs.curr_frame().locals
-    rs.print rs.jvm_carr2js_str(args[1], args[2], args[3])
-  'java/lang/Object::hashCode()I': (rs) ->
-    # return heap reference. XXX need to change this if we ever implement
-    # GC that moves stuff around.
-    rs.push rs.curr_frame().locals[0]
-}
+native_methods =
+  java:
+    lang:
+      Class: [
+        o 'getPrimitiveClass(Ljava/lang/String;)Ljava/lang/Class;', (rs) ->
+            str = rs.get_obj(rs.curr_frame().locals[0])
+            carr = rs.get_obj(str.obj.value).obj.array
+            name = (String.fromCharCode(c) for c in carr).join('') # XXX convert to unicode
+            rs.push rs.set_obj 'java/lang/Class', { name: name }
+        o 'getClassLoader0()Ljava/lang/ClassLoader;', (rs) -> rs.push 0  # we don't need no stinkin classloaders
+        o 'desiredAssertionStatus0(Ljava/lang/Class;)Z', (rs) -> rs.push 0 # we don't need no stinkin asserts
+        o 'getName0()Ljava/lang/String;', (rs) -> rs.push rs.init_string(rs.get_obj(rs.curr_frame().locals[0]).obj.name)
+        o 'forName0(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;', (rs) ->
+            jvm_str = rs.get_obj(rs.curr_frame().locals[0])
+            classname = rs.jvm2js_str(jvm_str).replace(/\./g,'/')
+            throw "Class.forName0: Failed to load #{classname}" unless rs.class_lookup(classname)
+            rs.push rs.set_obj 'java/lang/Class', { name:classname }
+        o 'getComponentType()Ljava/lang/Class;', (rs) ->
+            type = rs.get_obj(rs.curr_frame().locals[0]).obj.name
+            component_type = /\[+(.*)/.exec(type)[1]
+            rs.push rs.set_obj 'java/lang/Class', name:component_type
+      ],
+      Float: [
+        o 'floatToRawIntBits(F)I', (rs) ->  #note: not tested for weird values
+            f_val = rs.curr_frame().locals[0]
+            sign = if f_val < 0 then 1 else 0
+            f_val = Math.abs(f_val)
+            exp = Math.floor(Math.log(f_val)/Math.LN2)
+            sig = (f_val/Math.pow(2,exp)-1)/Math.pow(2,-23)
+            rs.push (sign<<31)+((exp+127)<<23)+sig
+      ]
+      Double: [
+        o 'doubleToRawLongBits(D)J', (rs) ->#note: not tested at all
+            d_val = rs.curr_frame().locals[0]
+            sign = if d_val < 0 then 1 else 0
+            d_val = Math.abs(d_val)
+            exp = Math.floor(Math.log(d_val)/Math.LN2)
+            sig = (d_val/Math.pow(2,exp)-1)/Math.pow(2,-52)
+            rs.push util.lshift(sign,63)+util.lshift(exp+1023,52)+sig, null
+      ]
+      Object: [
+        o 'getClass()Ljava/lang/Class;', (rs) ->
+            _this = rs.get_obj(rs.curr_frame().locals[0])
+            rs.push rs.set_obj 'java/lang/Class', { name:util.ext_classname _this.type}
+        o 'hashCode()I', (rs) ->
+            # return heap reference. XXX need to change this if we ever implement
+            # GC that moves stuff around.
+            rs.push rs.curr_frame().locals[0]
+      ]
+      reflect:
+        Array: [
+          o 'newArray(Ljava/lang/Class;I)Ljava/lang/Object;', (rs) ->
+            type = rs.get_obj(rs.curr_frame().locals[0]).obj.name
+            len = rs.curr_frame().locals[0]
+            rs.heap_newarray util.int_classname type, len
+        ]
+      StrictMath: [
+        o 'pow(DD)D', (rs) -> rs.push Math.pow(rs.cl(0),rs.cl(2)), null
+      ]
+      String: [
+        o 'intern()Ljava/lang/String;', (rs) ->
+            str_ref = rs.curr_frame().locals[0]
+            js_str = rs.jvm2js_str(rs.get_obj(str_ref))
+            unless rs.string_pool[js_str]
+              rs.string_pool[js_str] = str_ref
+              rs.push rs.string_pool[js_str]
+      ]
+      System: [
+        o 'arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V', (rs) ->
+            args = rs.curr_frame().locals
+            src_array = rs.get_obj(args[0]).obj.array
+            src_pos = args[1]
+            dest_array = rs.get_obj(args[2]).obj.array
+            dest_pos = args[3]
+            length = args[4]
+            j = dest_pos
+            for i in [src_pos...src_pos+length]
+              dest_array[j++] = src_array[i]
+        o 'currentTimeMillis()J', (rs) -> rs.push (new Date).getTime(), null
+        o 'initProperties(Ljava/util/Properties;)Ljava/util/Properties;', (rs) ->
+            p_ref = rs.curr_frame().locals[0]
+            m = rs.method_lookup({'class':'java/util/Properties','sig':{'name':'setProperty'}})
+            # properties to set:
+            #  java.version,java.vendor,java.vendor.url,java.home,java.class.version,java.class.path,
+            #  os.name,os.arch,os.version,file.separator,path.separator,
+            #  user.name,user.home,user.dir
+            props = {'file.encoding':'US_ASCII','java.vendor':'Coffee-JVM','line.separator':'\n'}
+            for k,v of props
+              rs.push p_ref, rs.init_string(k,true), rs.init_string(v,true)
+              m.run(rs)
+              rs.pop()  # we don't care about the return value
+            rs.push p_ref
+        o 'setIn0(Ljava/io/InputStream;)V', (rs) ->
+            rs.push rs.curr_frame().locals[0] # move oref to the stack for static_put
+            rs.static_put {'class':'java/lang/System','sig':{'name':'in'}}
+        o 'setOut0(Ljava/io/PrintStream;)V', (rs) ->
+            rs.push rs.curr_frame().locals[0] # move oref to the stack for static_put
+            rs.static_put {'class':'java/lang/System','sig':{'name':'out'}}
+        o 'setErr0(Ljava/io/PrintStream;)V', (rs) ->
+            rs.push rs.curr_frame().locals[0] # move oref to the stack for static_put
+            rs.static_put {'class':'java/lang/System','sig':{'name':'err'}}
+      ]
+      Thread: [
+        o 'currentThread()Ljava/lang/Thread;', (rs) -> rs.push rs.set_obj 'java/lang/Thread' # mock thread
+      ]
+      Throwable: [
+        o 'fillInStackTrace()Ljava/lang/Throwable;', (rs) ->
+            #TODO possibly filter out the java calls from our own call stack.
+            # at the moment, this is effectively a NOP.
+            rs.push rs.curr_frame().locals[0]
+      ]
+    security:
+      AccessController: [
+        o 'doPrivileged(Ljava/security/PrivilegedAction;)Ljava/lang/Object;', doPrivileged
+        o 'doPrivileged(Ljava/security/PrivilegedExceptionAction;)Ljava/lang/Object;', doPrivileged
+      ]
+    io:
+      FileSystem: [
+        o 'getFileSystem()Ljava/io/FileSystem;', (rs) -> rs.heap_new('java/io/UnixFileSystem')
+      ]
+      FileOutputStream: [
+        o 'writeBytes([BII)V', (rs) ->
+            args = rs.curr_frame().locals
+            rs.print rs.jvm_carr2js_str(args[1], args[2], args[3])
+      ]
+  sun:
+    misc:
+      VM: [
+        o 'initialize()V', (rs) ->  # NOP???
+      ]
+    reflect:
+      Reflection: [
+        o 'getCallerClass(I)Ljava/lang/Class;', (rs) ->
+            frames_to_skip = rs.curr_frame().locals[0]
+            #TODO: disregard frames assoc. with java.lang.reflect.Method.invoke() and its implementation
+            cls = rs.meta_stack[rs.meta_stack.length-1-frames_to_skip].class_name
+            rs.push rs.set_obj 'java/lang/Class', { name:cls }
+      ]
+
+flatten_pkg = (pkg) ->
+  result = {}
+  for pkg_name, inner_pkg of pkg
+    if inner_pkg instanceof Array
+      for method in inner_pkg
+        {fn_name, fn} = method
+        result["#{pkg_name}::#{fn_name}"] = fn
+    else
+      flattened_inner = flatten_pkg inner_pkg
+      for name, method of flattened_inner
+        fullname = "#{pkg_name}/#{name}"
+        result[fullname] = method
+  result
+
+trapped_methods = flatten_pkg trapped_methods
+native_methods = flatten_pkg native_methods

 array_methods =
   'getClass()Ljava/lang/Class;': (rs) ->


network-path reference for registration video

fixes CNVS-8847

Test Plan
1) Enable self-registration on your default account via the console
   (e.g. https://gist.github.com/JustAboutJeff/479e6fc120e5304ccf8e)
2) Navigate to localhost:3000/register_from_website
3) Click the 'Watch a Video' link and ensure the video plays as expected

Change-Id: Ic3e8f0de2066e2a73979c2a773143a6865f3e33c
Reviewed-on: https://gerrit.instructure.com/25407
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Jon Jensen <44f878afe53efc66b76772bd845eb65944ed8232@instructure.com>
Product-Review: Marc LeGendre <d33c80bc45d65303e33ca83108a9952b745af9ef@instructure.com>
QA-Review: Marc LeGendre <d33c80bc45d65303e33ca83108a9952b745af9ef@instructure.com>

--- app/coffeescripts/registration/main.coffee
+++ app/coffeescripts/registration/main.coffee
@@ -9,7 +9,7 @@
     signupDialog($(this).data('template'), $(this).prop('title'))

   $('#registration_video a').click preventDefault ->
-    $('

')
+    $('

')
       .dialog
         width: 800,
         title: "Canvas Introduction Video",


namespaced Movable

namespaced Movable

--- src/movable.coffee
+++ src/movable.coffee
@@ -42,6 +42,8 @@
   I.acceleration = Point(I.acceleration.x, I.acceleration.y)
   I.velocity = Point(I.velocity.x, I.velocity.y)

+  self.
+
   self.bind 'update.Movable', (dt) ->
     I.velocity = I.velocity.add(I.acceleration.scale(dt))


Removed unused EvidenceCollection

Removed unused EvidenceCollection

--- core/app/backbone/collections/evidence_collection.coffee
+++ core/app/backbone/collections/evidence_collection.coffee
@@ -1,31 +0,0 @@
-class window.EvidenceCollection extends Backbone.Factlink.Collection
-
-  initialize: (models, options) ->
-    @fact = options.fact
-
-    @realEvidenceCollection = new RealEvidenceCollection null, fact: @fact
-
-    @_containedCollections = [
-      new OpinionatersCollection null, fact: @fact
-      @realEvidenceCollection
-    ]
-
-    for collection in @_containedCollections
-      @listenTo collection, 'sync', @loadFromCollections
-      @listenTo collection, 'add', (model) -> @add model
-      @listenTo collection, 'remove', (model) -> @remove model
-
-  comparator: (item) -> -item.get('impact')
-
-  fetch: (options={}) ->
-    @trigger 'request', @
-    _.invoke @_containedCollections, 'fetch', _.extend {}, options, reset: true
-
-  _sub_loading: ->
-    _.some @_containedCollections, (collection) -> collection.loading()
-
-  loadFromCollections: (collectionOrModel) ->
-    return if @_sub_loading() || !@loading()
-
-    @reset(_.union (col.models for col in @_containedCollections)...)
-    @trigger 'sync', @


Map models instead of looping

Map models instead of looping

--- core/app/backbone/collections/evidence_collection.coffee
+++ core/app/backbone/collections/evidence_collection.coffee
@@ -5,16 +5,14 @@
     @fact = opts.fact

   parse: (data) ->
-    results = []
-    _.each data, (item) =>
+    _.map data, (item) ->
       switch item.evidence_type
         when 'FactRelation'
-          results.push new FactRelation(item)
+          new FactRelation(item)
         when 'Comment'
-          results.push new Comment(item)
+          new Comment(item)
         else
           console.error "Evidence type not supported: #{item.evidence_type}"
-    results

   url:     -> "#{@baseUrl()}/combined"
   baseUrl: -> "/facts/#{@fact.id}/#{@type}_evidence"


Don't pass class name, parent or id attributes

--- layout.coffee
+++ layout.coffee
@@ -270,9 +270,8 @@
 #			console.log("module included: ", @, module)

+	ignoredAttributes = {parent: true, id: true, name: true}
 	class View extends Node
-		skipStyle= {parent: true, id: true, name: true}
-
 		constructor: (el, options = {}) ->
 			if (el instanceof HTMLElement and el.$view)
 				console.warn 'already bound view', el.$view, el
@@ -287,7 +286,7 @@
 #			console.log 'new view', el, options, @

 		setAttribute: (name, value, skipsend) ->
-			if (skipsend or skipStyle[name] or value == this[name])
+			if (skipsend or ignoredAttributes[name] or value == this[name])
 #				console.log 'skipping style', name, this[name], value, @
 			else
 				@sprite.setStyle(name, value)
@@ -348,7 +347,9 @@
 	class Class
 		constructor: (el, options = {}) ->
 			name = options.name
-			delete options.name
+			for ignored of ignoredAttributes
+				delete options[ignored]
+
 			body = el.innerHTML
 			el.innerHTML = ''
 #			console.log('new class', name, options)


simplify account wording

simplify account wording

--- app/js/directives/misc.coffee
+++ app/js/directives/misc.coffee
@@ -24,7 +24,7 @@

 angular.module("app.directives").directive "inputName", ->
-  template: '<input placeholder="Account Name (Required)" autofocus ng-model="$parent.ngModel" ng-blur="removeTrailingDashes()" my-ng-enter="removeTrailingDashes()" popover="Name can only contain lowercase alphanumeric characters and dashes, must start with a letter, and cannot end with a dash.  It can be at most 63 characters long.  Invalid characters are blocked and letters are lowercased automatically."  popover-append-to-body="true" popover-trigger="focus" ng-keydown="kd()" ng-change="ku()" uncapitalize type="text" class="form-control">'
+  template: '<input placeholder="Account Name (Required)" autofocus ng-model="$parent.ngModel" ng-blur="removeTrailingDashes()" my-ng-enter="removeTrailingDashes()" popover="Name can only contain lowercase alphanumeric characters and dashes, must start with a letter, and cannot end with a dash."  popover-append-to-body="true" popover-trigger="focus" ng-keydown="kd()" ng-change="ku()" uncapitalize type="text" class="form-control">'
   restrict: "E"
   scope:
     ngModel: "="


Adjust budget progress values

Adjust budget progress values

--- app/js/lib/application.coffee
+++ app/js/lib/application.coffee
@@ -15,14 +15,14 @@
 window.keptBudgetPercentage = (percentage) ->
   val = parseInt(percentage)
   if val > 100
-    200 - val
+    100 / val * 100
   else
     val

 window.exceededBudgetPercentage = (percentage) ->
   val = parseInt(percentage)
   if val > 100
-    val - 100
+    (val - 100) / val * 100
   else
     0


activity: mark a channel opened once its messages are loaded

--- client/activity/lib/flux/stores/openedchannelsstore.coffee
+++ client/activity/lib/flux/stores/openedchannelsstore.coffee
@@ -13,7 +13,7 @@

   initialize: ->

-    @on actions.LOAD_CHANNEL_SUCCESS, (openedChannels, { channelId }) ->
+    @on actions.LOAD_MESSAGES_SUCCESS, (openedChannels, { channelId }) ->
       openedChannels.set channelId, yes


Test correct editor in font size spec

Test correct editor in font size spec

--- spec/app/editor-spec.coffee
+++ spec/app/editor-spec.coffee
@@ -320,8 +320,9 @@
         expect(editor.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editor.lineHeight

         newEditor = new Editor(editor.activeEditSession.copy())
+        editor.remove()
         newEditor.attachToDom()
-        expect(editor.css('font-size')).toBe '30px'
+        expect(newEditor.css('font-size')).toBe '30px'

       it "updates the position and size of selection regions", ->
         config.set("editor.fontSize", 10)


Fix bug on stream.remove event, zoom and unzoom when clicking on zoom button on feeds cameras

--- src/frontend/modules/camera.coffee
+++ src/frontend/modules/camera.coffee
@@ -21,7 +21,7 @@
         do @appendHTML
         @jqMainCams       = ($ '#mainCam')
         @jqVideoContainer = ($ 'ul#videos')
-        @currentZoom      = undefined
+        @zoomCams         = { }
         @streams          = [ ]
         ($ 'button#joinConference').bind 'click', @enableCamera
         @emitter.on 'stream.create', @newStream
@@ -55,8 +55,10 @@
         if (@streams.indexOf user.id) >= 0
             do ($ "li#video_#{user.id}").remove
             @streams.splice user.id, 1
-            if @currentZoom is user.id
-                @zoom undefined, undefined
+            for k, v of @zoomCams
+                if k is user.id
+                    @zoom user.id, undefined
+                    return

     newStream : (event, data) =>
         @streams.push data.uid
@@ -66,7 +68,7 @@
         video.addClass 'thumbnailVideo'
         video.attr 'rel', data.uid
         @emitter.trigger 'stream.display', video
-        if not @currentZoom? and (not data.local? or not data.local)
+        if Object.keys(@zoomCams).length == 0 and (not data.local? or not data.local)
             @zoom data.uid, video

     changeStreamState : (event, data) =>
@@ -86,14 +88,20 @@
     zoom : (uid, video) =>
         container    = ($ 'div#mainCam')
         container.removeClass 'hidden'
-        do (container.find 'video').remove
-        @currentZoom = uid
+        for k, v of @zoomCams
+            if k is uid
+                do @zoomCams[uid].remove
+                delete @zoomCams[uid]
+                return
         if video?
             newVideo     = do video.clone
+            newVideo.attr 'id', "zoomcam#{uid}"
             newVideo[0].volume = video[0].volume
             newVideo.removeClass 'thumbnailVideo'
             do newVideo[0].play
             container.append newVideo
+            @zoomCams[uid] = ($ "video#zoomcam#{uid}")
+

 if window?
     window.Camera = Camera


Replace Consolas with PCMyungjo in spec

Replace Consolas with PCMyungjo in spec

--- spec/app/editor-spec.coffee
+++ spec/app/editor-spec.coffee
@@ -554,13 +554,12 @@
         expect(newEditor.css('font-family')).toBe 'Courier'

       it "updates the font family of editors and recalculates dimensions critical to cursor positioning", ->
-        rootView.attachToDom()
-        rootView.height(200)
-        rootView.width(200)
+        editor.attachToDom(12)

         lineHeightBefore = editor.lineHeight
         charWidthBefore = editor.charWidth
-        config.set("editor.fontFamily", "Consolas")
+        config.set("editor.fontFamily", "PCMyungjo")
+
         editor.setCursorScreenPosition [5, 6]
         expect(editor.charWidth).not.toBe charWidthBefore
         expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth }


Un-f a window spec

Un-f a window spec

--- spec/app/window-spec.coffee
+++ spec/app/window-spec.coffee
@@ -17,7 +17,7 @@
     it "doesn't have .is-blurred on the body tag", ->
       expect($("body")).not.toHaveClass("is-blurred")

-  fdescribe "when the window is blurred", ->
+  describe "when the window is blurred", ->
     beforeEach ->
       $(window).trigger 'blur'


fix(build option): inline.jsHtmlImports should match only .html imports when js is all on one line, fix for prod build


Remove spec about saving a destroyed editor

This is now explicitly forbidden. See https://github.com/atom/text-buffer/pull/193

--- spec/whitespace-spec.coffee
+++ spec/whitespace-spec.coffee
@@ -26,11 +26,6 @@
     beforeEach ->
       editor.destroy()

-    it "unsubscribes from the buffer", ->
-      buffer.setText("foo   \nbar\t   \n\nbaz")
-      buffer.save()
-      expect(buffer.getText()).toBe "foo   \nbar\t   \n\nbaz"
-
     it "does not leak subscriptions", ->
       {whitespace} = atom.packages.getActivePackage('whitespace').mainModule
       expect(whitespace.subscriptions.disposables.size).toBe 2


Build task creating zip file for Chrome Web Store

--- Jakefile.coffee
+++ Jakefile.coffee
@@ -65,6 +65,14 @@
   logStatus()
 , true

+desc "Build zip for Chrome Web Store"
+task 'build', ->
+  spawn = require('child_process').spawn 'zip', ['-r', 'omnicat.zip', 'ext']
+  spawn.stderr.on 'data', (data) -> process.stderr.write(data)
+  spawn.stdout.on 'data', (data) -> process.stdout.write(data)
+  spawn.on 'exit', complete
+, true
+

 updateRepos = (db, repos, callback) ->
   ids = repos.map (repo) -> "#{repo.owner}:#{repo.name}"


Fix for gecko

Fix for gecko

--- results/front/app/coffee/controllers/main.coffee
+++ results/front/app/coffee/controllers/main.coffee
@@ -71,7 +71,7 @@
     if $rootScope.year==0
         $scope.today();
     else
-        $scope.dt = new Date($rootScope.year+'-'+$rootScope.month+'-'+$rootScope.day)
+        $scope.dt = new Date($rootScope.year, $rootScope.month-1, $rootScope.day)

     $scope.startingDay = 1
     $scope.showWeeks = false


Make sure message count is updated everywhere

--- assets/javascripts/mailcatcher.js.coffee
+++ assets/javascripts/mailcatcher.js.coffee
@@ -54,6 +54,7 @@
           type: "DELETE"
           success: =>
             @unselectMessage()
+            @updateMessagesCount()
           error: ->
             alert "Error while clearing all messages."

@@ -111,6 +112,7 @@
               @loadMessage switchTo
             else
               @unselectMessage()
+            @updateMessagesCount()

           error: ->
             alert "Error while removing message."
@@ -118,6 +120,7 @@

     @refresh()
     @subscribe()
+    @updateMessagesCount()

   # Only here because Safari's Date parsing *sucks*
   # We throw away the timezone, but you could use it for something...
@@ -138,6 +141,10 @@

   messagesCount: ->
     $("#messages tr").length - 1
+
+  updateMessagesCount: ->
+    title = $("head title")
+    title.text(title.text().replace(/^\(\d*\)/, "(#{@messagesCount()})"))

   tabs: ->
     $("#message ul").children(".tab")
@@ -190,8 +197,7 @@
       .append($("<td/>").text(message.subject or "No subject").toggleClass("blank", !message.subject))
       .append($("<td/>").text(@formatDate(message.created_at)))
       .prependTo($("#messages tbody"))
-    title = $('head title')
-    title.text(title.text().replace(/^\(\d*\)/, "(#{@messagesCount()})"))
+    @updateMessagesCount()

   scrollToRow: (row) ->
     relativePosition = row.offset().top - $("#messages").offset().top


Update comment

Update comment

--- core/app/backbone/views/wheel/persistent_wheel_view.coffee
+++ core/app/backbone/views/wheel/persistent_wheel_view.coffee
@@ -9,6 +9,7 @@

     # TODO: Remove global jQuery call. You can currently have only one
     #       persistent wheel view per page
+    #       Even better: look into this, probably is not needed.
     $("input[name=opinion][value=#{opinionType.type}s]")
       .prop 'checked', true


Fix some listeners

Fix some listeners

--- admin/static/coffee/namespaces/namespace.coffee
+++ admin/static/coffee/namespaces/namespace.coffee
@@ -415,6 +415,14 @@
         delete_namespace: (event) ->
             event.preventDefault()
             @remove_namespace_dialog = new NamespaceView.RemoveNamespaceModal
+            #overwrite on_success to add a redirection
+            @remove_namespace_dialog.on_success = (response) ->
+                if (response)
+                    @on_success_with_error()
+                    return
+                super
+
+
             @remove_namespace_dialog.render [@model]

         render: =>


Make Metric.power_of_thousand work when given 0.

--- app/assets/javascripts/lib/models/metric.coffee
+++ app/assets/javascripts/lib/models/metric.coffee
@@ -125,7 +125,8 @@

   # 0-999: 0, 1000-999999: 1, ...
   power_of_thousand: (x) ->
-    return parseInt(Math.log(Math.abs(x)) / Math.log(1000))
+    if x is 0 then 0 else
+      parseInt(Math.log(Math.abs(x)) / Math.log(1000), 10)

   # Returns the string currently used on the i18n file
   power_of_thousand_to_string: (x) -> @scale_label["#{x}"]


disabling quantity when unlimited quantity is clicked and vice-versa

--- app/assets/javascripts/mobile/_mobile.js.coffee
+++ app/assets/javascripts/mobile/_mobile.js.coffee
@@ -186,5 +186,9 @@

 if gon.page is 'products_new' or gon.page is 'products_create' or gon.page is 'products_edit' or gon.page is 'products_update'
     $ ->
+        ($ '#product_unlimited_quantity').change ->
+            disabled = if ($ @).prop('checked') then 'disabled' else ''
+            ($ '#product_quantity').prop('disabled', disabled)
+
         ($ '.mobile-form').isHappy(product_form_validation_rules)


Fixed the Couchbase server to shutdown the connection!

--- server/src/Component/Builtin/QueryBuilder.coffee
+++ server/src/Component/Builtin/QueryBuilder.coffee
@@ -19,7 +19,8 @@

     selectStarFrom: (table) ->
         throw new Exceptions.IllegalArgument() if table is undefined
-        @query += "SELECT * FROM " + table + " "
+        @query += "SELECT * FROM " + table + " " if !@n1ql
+        @query += "SELECT META(), VALUE() FROM " + table + " " if @n1ql
         this

     select: ->


Fix WPM calculation

Fix WPM calculation

--- app/assets/javascripts/typist-teacher.js.coffee
+++ app/assets/javascripts/typist-teacher.js.coffee
@@ -17,9 +17,9 @@
     timeCurrent = new Date().getTime()
     unless iLastTime is 0
       keycount++
-      wordcount = $("#typist").val().split(" ").length
+      wordcount = window.wordcount
       timeTotal += timeCurrent - iLastTime
-      cpm = Math.round(keycount / timeTotal * 6000, 2)
+      cpm = Math.round(keycount / timeTotal * 60000, 2)
       wpm = Math.round(wordcount / timeTotal * 6000, 2)
     iLastTime = timeCurrent
     return { "wpm": wpm, "cpm": cpm }
@@ -40,26 +40,28 @@
         $('body').append "Successful AJAX call: #{practice_data}"

   updatePracticeText = (text) ->
-    if text is arr[0] + " "
-      arr.shift()
-      text = arr.join(" ")
-      $("#trainer").text text
-      $("#typist").val ""
-      return false
+    arr.shift()
+    text = arr.join(" ")
+    $("#trainer").text text
+    $("#typist").val ""
+    return false

   startPractice: (tracker, lesson_id = 1) ->
     trainer = $("#trainer")
     typist = $("#typist")
     arr = []
+    window.wordcount = 0
     $.get "/lessons/#{lesson_id}.json", (lesson_data) ->
-      #console.log(lesson_data)
       arr = lesson_data.text.split(" ")
       trainer.text lesson_data.text

     typist.keydown (e) ->
       currentKey = String.fromCharCode(e.which).toLowerCase()
       inputText = typist.val() + currentKey
-      updatePracticeText(inputText)
+
+      if inputText is arr[0] + " "
+        window.wordcount += 1
+        updatePracticeText(inputText)

     typist.keyup ->
       data = tracker.printSpeed()


cleanup

cleanup

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -86,17 +86,11 @@
   onDidMoveToPrompt: (fn) -> @emitter.on('did-move-to-prompt', fn)
   emitDidMoveToPrompt: -> @emitter.emit('did-move-to-prompt')

-  onDidRequestItems: (fn) -> @emitter.on('did-request-items', fn)
-  emitDidRequestItems: (event) -> @emitter.emit('did-request-items', event)
-
   onDidUpdateItems: (fn) -> @emitter.on('did-update-items', fn)
   emitDidUpdateItems: (event) -> @emitter.emit('did-update-items', event)

   onFinishUpdateItems: (fn) -> @emitter.on('finish-update-items', fn)
   emitFinishUpdateItems: -> @emitter.emit('finish-update-items')
-
-  onCancelRequestItems: (fn) -> @emitter.on('cancel-request-items', fn)
-  emitCancelRequestItems: -> @emitter.emit('cancel-request-items')

   onDidMoveToItemArea: (fn) -> @emitter.on('did-move-to-item-area', fn)
   emitDidMoveToItemArea: -> @emitter.emit('did-move-to-item-area')
@@ -142,8 +136,8 @@
       'narrow-ui:confirm-keep-open': => @confirm(keepOpen: true)
       'narrow-ui:protect': => @toggleProtected()
       'narrow-ui:preview-item': => @preview()
-      'narrow-ui:preview-next-item': => @previewNextItem()
-      'narrow-ui:preview-previous-item': => @previewPreviousItem()
+      'narrow-ui:preview-next-item': => @previewItemForDirection('next')
+      'narrow-ui:preview-previous-item': => @previewItemForDirection('previous')
       'narrow-ui:toggle-auto-preview': @toggleAutoPreview
       'narrow-ui:move-to-prompt-or-selected-item': => @moveToPromptOrSelectedItem()
       'narrow-ui:move-to-prompt': => @moveToPrompt()
@@ -498,12 +492,6 @@
       @items.selectItemForRow(row)
       @preview()

-  previewNextItem: ->
-    @previewItemForDirection('next')
-
-  previewPreviousItem: ->
-    @previewItemForDirection('previous')
-
   getQuery: ->
     @editor.getTextInBufferRange(@getPromptRange())


cleanup

cleanup

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -431,18 +431,16 @@
       if @excludedFiles.length
         items = items.filter ({filePath}) => filePath not in @excludedFiles

-      selectedItem = @items.getSelectedItem()
+      oldSelectedItem = @items.getSelectedItem()
       @items.setItems([@promptItem, items...])
       @renderItems(items)

-      if (not selectFirstItem) and item = @items.findItem(selectedItem)
-        @items.selectItem(item)
+      if @items.hasNormalItem()
+        if (not selectFirstItem) and item = @items.findItem(oldSelectedItem)
+          @items.selectItem(item)
+        else
+          @items.selectFirstNormalItem()
       else
-        @items.selectFirstNormalItem()
-
-      @setModifiedState(false)
-      unless @items.hasNormalItem()
-        @items.reset()
         @highlighter.clearLineMarker()

       @emitDidRefresh()
@@ -458,6 +456,7 @@
         @providerPanel.show() # redraw providerPanel block decoration.
       itemArea = new Range(@itemAreaStart, @editor.getEofBufferPosition())
       range = @editor.setTextInBufferRange(itemArea, texts.join("\n"), undo: 'skip')
+      @setModifiedState(false)
       @editorLastRow = range.end.row

   debouncedPreview: ->


Omit outputting empty elements.

Omit outputting empty elements.

--- src/scraper.coffee
+++ src/scraper.coffee
@@ -126,17 +126,22 @@

               children = []
               $(".comment-replies-renderer .comment-renderer", @).each (i) ->
-                children.push
+                child =
                   comment: $(".comment-renderer-text-content", @).text()
                   like: check_like_score $(".comment-renderer-like-count.off", @).text()
                   author: $(@).data("author-name")
-                  receiver: $(".comment-renderer-text-content", @).find("a").text()
+                receiver = $(".comment-renderer-text-content", @).find("a").text()
+                if receiver isnt ""
+                  child.receiver = receiver
+                children.push child

-              res.push
+              comment =
                 root: $(".comment-renderer-text-content", root).text()
                 author: root.data("author-name")
                 like: check_like_score $(".comment-renderer-like-count.off", root).text()
-                children: children
+              if children.length isnt 0
+                comment.children = children
+              res.push comment

             # Clean up.
             page.close()


happy-path for send-message-as

happy-path for send-message-as

--- test/integration/send-message-2-as-spec.coffee
+++ test/integration/send-message-2-as-spec.coffee
@@ -9,7 +9,8 @@
 JobManager     = require 'meshblu-core-job-manager'
 HydrantManager = require 'meshblu-core-manager-hydrant'

-xdescribe 'SendMessage2: send-as', ->
+describe 'SendMessage2: send-as', ->
+  @timeout 5000
   beforeEach (done) ->
     @db            = mongojs 'meshblu-core-test'
     @devices       = @db.collection 'devices'
@@ -29,9 +30,6 @@
     client.del 'request:queue', done

   beforeEach 'create sender device', (done) ->
-    @auth =
-      uuid: 'imposter-uuid'
-      token: 'leak'

     @senderDevice =
       uuid: 'sender-uuid'
@@ -59,19 +57,18 @@
   beforeEach 'create imposter device', (done) ->
     @imposterDevice =
       uuid: 'imposter-uuid'
-      token: bcrypt.hashSync @auth.token, 8
+      token: bcrypt.hashSync 'leak', 8
       type: 'device:imposter'

     @devices.insert @imposterDevice, done

   context 'When sending a message as another device', ->
     context "sender-uuid receiving it's sent messages", ->
-      @timeout 5000
       beforeEach 'create message received subscription', (done) ->
         subscription =
           type: 'message.received'
-          emitterUuid: 'received-uuid'
-          subscriberUuid: 'received-uuid'
+          emitterUuid: 'receiver-uuid'
+          subscriberUuid: 'receiver-uuid'

         @subscriptions.insert subscription, done

@@ -83,6 +80,7 @@
               uuid: 'imposter-uuid'
               token: 'leak'
               as: 'sender-uuid'
+            fromUuid: 'sender-uuid'
             jobType: 'SendMessage2'
           data:
             devices: ['receiver-uuid'], payload: 'boo'


Moved to new email channel urls.

Moved to new email channel urls.

--- app/assets/javascripts/app/controllers/getting_started.coffee
+++ app/assets/javascripts/app/controllers/getting_started.coffee
@@ -474,7 +474,7 @@
     @ajax(
       id:   'email_notification'
       type: 'POST'
-      url:  "#{@apiPath}/channels/email_notification"
+      url:  "#{@apiPath}/channels_email_notification"
       data: JSON.stringify(params)
       processData: true
       success: (data, status, xhr) =>
@@ -751,7 +751,7 @@
     @ajax(
       id:   'email_probe'
       type: 'POST'
-      url:  "#{@apiPath}/channels/email_probe"
+      url:  "#{@apiPath}/channels_email_probe"
       data: JSON.stringify(params)
       processData: true
       success: (data, status, xhr) =>
@@ -800,7 +800,7 @@
     @ajax(
       id:   'email_inbound'
       type: 'POST'
-      url:  "#{@apiPath}/channels/email_inbound"
+      url:  "#{@apiPath}/channels_email_inbound"
       data: JSON.stringify(params)
       processData: true
       success: (data, status, xhr) =>
@@ -853,7 +853,7 @@
     @ajax(
       id:   'email_outbound'
       type: 'POST'
-      url:  "#{@apiPath}/channels/email_outbound"
+      url:  "#{@apiPath}/channels_email_outbound"
       data: JSON.stringify(params)
       processData: true
       success: (data, status, xhr) =>
@@ -881,7 +881,7 @@
     @ajax(
       id:   'email_verify'
       type: 'POST'
-      url:  "#{@apiPath}/channels/email_verify"
+      url:  "#{@apiPath}/channels_email_verify"
       data: JSON.stringify(account)
       processData: true
       success: (data, status, xhr) =>


Update jquery.china_city.js.coffee

Update jquery.china_city.js.coffee

--- app/assets/javascripts/china_city/jquery.china_city.js.coffee
+++ app/assets/javascripts/china_city/jquery.china_city.js.coffee
@@ -8,6 +8,7 @@
         $("option:gt(0)", next_selects).remove()
         if next_selects.first()[0] and $this.val() # init next child
           $.get "/china_city/#{$(@).val()}", (data) ->
+            data = data.data if data.data?
             next_selects.first()[0].options.add(new Option(option[0], option[1])) for option in data
             # init value after data completed.
             next_selects.trigger('china_city:load_data_completed');


Added "process.stdin.resume()" to fix falling tests

--- test/bin/mock.coffee
+++ test/bin/mock.coffee
@@ -8,6 +8,7 @@
     rawBody process.stdin, {encoding: 'utf8'}, (err, data) ->
       argv.data = data unless err
       next()
+    process.stdin.resume()
   else
     try argv.data = String fs.readFileSync source
     next()


cooldown finilazation

cooldown finilazation

--- test/cooldown.coffee
+++ test/cooldown.coffee
@@ -100,6 +100,6 @@

 # TODO: tests to make sure value isn't overwritten if the instance variable already exists
 # TODO: tests to make sure value is defaulted to 0, but only if the instance variable doesn't exist
-test "initialize property to 0 if no current "
+test "initialize property to 0 if no current value", -

 module()


Min fix ...

Min fix ...

--- app/assets/javascripts/zahlen/subscription_checkout.js.coffee
+++ app/assets/javascripts/zahlen/subscription_checkout.js.coffee
@@ -45,13 +45,13 @@
     self = @
     form_selector = @constructor.form_selector
     $('body').on "change", "input[data-zahlen='payment-method']", (e) ->
+      payment_method = $(this).val().replace(/_/, '-')
       self.payment_method = payment_method
       self.displayPaymentMethodData()

   displayPaymentMethodData: ->
-    payment_method = @payment_method.replace(/_/, '-')
     $('.payment-method-fields').hide()
-    $(".#{payment_method}-fields").show()
+    $(".#{@payment_method}-fields").show()

   validateForm: ->
     self = @


Don't uninstall apm during update

Don't uninstall apm during update

--- src/updater.coffee
+++ src/updater.coffee
@@ -14,6 +14,7 @@
     dependencies ?= {}
     for installedModule in fs.list('node_modules')
       continue if installedModule is '.bin'
+      continue if installedModule is 'atom-package-manager'
       continue if dependencies.hasOwnProperty(installedModule)
       continue if devDependencies.hasOwnProperty(installedModule)


array_extensions

--- src/array_extensions.coffee
+++ src/array_extensions.coffee
@@ -50,8 +50,7 @@

 @name clear
 @methodOf Array#
-@type Array
-@returns this, now emptied.
+@returns {Array} this, now emptied.
 ###
 Array::clear = ->
   this.length = 0


fix mergeAccounts method to migrate shifts as well

--- both/methods/user/userPermissions.coffee
+++ both/methods/user/userPermissions.coffee
@@ -16,17 +16,70 @@

 			Meteor.users.remove oldUser._id

-	mergeAccounts: (oldUserId, newUserId) -> if oldUserId != newUserId
+	mergeAccounts: (oldUserId, newUserId) -> if newUserId == Meteor.userId() && oldUserId != newUserId
 		oldUser = Meteor.users.findOne oldUserId,
 			fields: roles: 1

 		newUser = Meteor.users.findOne newUserId,
-			fields: _id: 1
+			fields: profile: 1

 		if oldUser? && newUser?
 			for id in Object.keys(oldUser.roles)
 				if oldUser.roles[id][0]
 					Roles.addUsersToRoles newUserId, oldUser.roles[id][0], id
+
+			shifts = Shifts.find({
+				$or: [{
+					'teams.participants._id': oldUserId
+				}, {
+					'teams.pending._id': oldUserId
+				}, {
+					'teams.declined._id': oldUserId
+				}]
+			}).fetch()
+
+			newShiftUser =
+				_id: newUserId
+				name: newUser.profile.firstname + ' ' + newUser.profile.lastname
+				phone: newUser.profile.telefon
+				email: newUser.profile.email
+				thisTeamleader: false
+
+			shifts.forEach (shift) ->
+				shift.teams = shift.teams.map (team) ->
+					team.participants = team.participants.map (user) ->
+						if user?._id == oldUserId
+							if user.thisTeamleader
+								user = newShiftUser
+								user.thisTeamleader = true
+							else
+								user = newShiftUser
+							user.teamleader = Roles.userIsInRole newUserId, 'teamleader', shift.tagId
+							user.substituteTeamleader = Roles.userIsInRole newUserId, 'substituteTeamleader', shift.tagId
+
+						return user
+
+					team.pending = team.pending.map (user) ->
+						if user?._id == oldUserId
+							user = newShiftUser
+							user.thisTeamleader = false
+							user.teamleader = Roles.userIsInRole newUserId, 'teamleader', shift.tagId
+							user.substituteTeamleader = Roles.userIsInRole newUserId, 'substituteTeamleader', shift.tagId
+
+						return user
+
+					team.declined = team.declined.map (user) ->
+						if user?._id == oldUserId
+							user = newShiftUser
+							user.thisTeamleader = false
+							user.teamleader = Roles.userIsInRole newUserId, 'teamleader', shift.tagId
+							user.substituteTeamleader = Roles.userIsInRole newUserId, 'substituteTeamleader', shift.tagId
+
+						return user
+
+					return team
+
+				Shifts.update({ _id: shift._id }, shift)

 			Meteor.users.remove oldUser._id


test: test if script handles errors correctly

--- test/run.spec.coffee
+++ test/run.spec.coffee
@@ -2,6 +2,7 @@
 chai.use require 'sinon-chai'
 expect = chai.expect
 sinon = require 'sinon'
+domain = require 'domain'

 proxyquire = require 'proxyquire'
 mockFs = require 'mock-fs'
@@ -29,9 +30,7 @@
     proxyquire '../lib/run.js',
       '@semantic-release/last-release-npm': lastRelease
       'fs': fs
-
-    # Check
-    expects = ->
+    .then ->
       expect(lastRelease.args[0][1]).to.deep.equal
         pkg: pkg
         npm:
@@ -46,4 +45,17 @@

       done()

-    setTimeout expects, 100
+  it "throws an error if anything goes wrong", (done) ->
+    # Setup
+    pkg =
+      name: "ModuleName"
+    fs.writeFileSync './package.json', JSON.stringify pkg
+
+    lastRelease.yields "Some Error"
+
+    proxyquire '../lib/run.js',
+      '@semantic-release/last-release-npm': lastRelease
+      'fs': fs
+    .then ->
+      expect(process.exitCode).to.equal 1
+      done()


Reusable `setOptionHighlight` method

Reusable `setOptionHighlight` method

--- select.coffee
+++ select.coffee
@@ -101,8 +101,7 @@
             trigger: 'click'

         @dropSelect.$drop.on 'dropopen', =>
-            @dropSelect.$drop.find('.drop-select-option-highlight').removeClass('drop-select-option-highlight')
-            @getSelectedOption().addClass('drop-select-option-highlight')
+            @setOptionHighlight @getSelectedOption()[0]

     renderDrop: ->
         $dropSelectOptions = $ '

'
@@ -142,12 +141,15 @@
                     return
             else
                 if $option.text().toLowerCase().substr(0, text.length) is text.toLowerCase()
-                    that.dropSelect.$drop.find('.drop-select-option-highlight').removeClass('drop-select-option-highlight')
-                    $option.addClass 'drop-select-option-highlight'
+                    @setOptionHighlight option
                     return

             optionsChecked += 1
             i += 1
+
+    setOptionHighlight: (option) ->
+        @dropSelect.$drop.find('.drop-select-option-highlight').removeClass('drop-select-option-highlight')
+        $(option).addClass('drop-select-option-highlight')

     selectHighlightedOption: ->
         @selectOption @dropSelect.$drop.find('.drop-select-option-highlight')[0]
@@ -171,8 +173,7 @@
             else if direction is 'down'
                 $newSelection = $currentSelection

-        @dropSelect.$drop.find('.drop-select-option-highlight').removeClass('drop-select-option-highlight')
-        $newSelection.addClass('drop-select-option-highlight')
+        @setOptionHighlight $newSelection[0]

     selectOption: (option) ->
         @$select.val($(option).data('value'))
@@ -193,7 +194,6 @@
             @selectOption e.target

         @dropSelect.$drop.on 'mousemove', '.drop-select-option', (e) =>
-            @dropSelect.$drop.find('.drop-select-option-highlight').removeClass('drop-select-option-highlight')
-            $(e.target).addClass('drop-select-option-highlight')
+            @setOptionHighlight e.target

 window.Select = Select


don't minify files that already end with ".min.js"

--- server.coffee
+++ server.coffee
@@ -54,11 +54,12 @@
       if !fs.existsSync 'cache' then fs.mkdirSync 'cache'
       s = fs.readFileSync f, 'utf8'
       if /\.coffee$/.test f then log "compiling #{f}"; s = coffee.compile s, bare: 1
-      log "minifying #{f}"
-      s1 = uglify.minify(s, fromString: true, mangle: false).code
-      try fs.writeFileSync f1, s1 catch # ignore errors
-      log "  #{s.length} -> #{s1.length} bytes"
-      s = s1
+      if !/\.min\.js$/.test f
+        log "minifying #{f}"
+        s1 = uglify.minify(s, fromString: true, mangle: false).code
+        try fs.writeFileSync f1, s1 catch # ignore errors
+        log "  #{s.length} -> #{s1.length} bytes"
+        s = s1
     else
       s = fs.readFileSync f1, 'utf8'
     js += s + '\n'


Style.

Style.

--- server.coffee
+++ server.coffee
@@ -22,16 +22,16 @@
 globals.Document.Migrations = new Meteor.Collection 'peerdb.migrations'

 globals.Document._Migration = class
-    updateAll: (document, collection, currentSchema, intoSchema) =>
-      @_updateAll = true
-
-    forward: (document, collection, currentSchema, newSchema) =>
-      migrated: 0
-      all: collection.update {_schema: currentSchema}, {$set: _schema: newSchema}, {multi: true}
-
-    backward: (document, collection, currentSchema, oldSchema) =>
-      migrated: 0
-      all: collection.update {_schema: currentSchema}, {$set: _schema: oldSchema}, {multi: true}
+  updateAll: (document, collection, currentSchema, intoSchema) =>
+    @_updateAll = true
+
+  forward: (document, collection, currentSchema, newSchema) =>
+    migrated: 0
+    all: collection.update {_schema: currentSchema}, {$set: _schema: newSchema}, {multi: true}
+
+  backward: (document, collection, currentSchema, oldSchema) =>
+    migrated: 0
+    all: collection.update {_schema: currentSchema}, {$set: _schema: oldSchema}, {multi: true}

 globals.Document.PatchMigration = class extends globals.Document._Migration


server support for multiple browser connections

--- server.coffee
+++ server.coffee
@@ -87,47 +87,55 @@
 toInterpreter 'SupportedProtocols=1'
 toInterpreter 'UsingProtocol=1'

+cmd = (name, args) -> toInterpreter """<?xml version="1.0" encoding="utf-8"?><Command><cmd>#{name}</cmd><id>0</id><args><#{name}>#{args}</#{name}></args></Command>"""
+
+sockets = [] # list of socket.io connections to browsers
+
+toBrowser = (m...) ->
+  log 'to browser: ' + JSON.stringify(m)[..1000]
+  for socket in sockets then socket.emit m...
+  return
+
+queue = Buffer 0 # a buffer for data received from the server
+client.on 'data', (data) ->
+  queue = Buffer.concat [queue, data]
+  while queue.length >= 4 and (n = queue.readInt32BE 0) <= queue.length
+    m = '' + queue[8...n]; queue = queue[n..]
+    log 'from interpreter: ' + JSON.stringify(m)[..1000]
+    if !/^(?:SupportedProtocols|UsingProtocol)=1$/.test m # ignore these
+      switch (/^<(\w+)>/.exec m)?[1] or ''
+        when 'ReplyConnect', 'ReplyNotAtInputPrompt', 'ReplyEdit', 'ReplySaveChanges', 'ReplySetLineAttributes' then ; # ignore
+        when 'ReplyIdentify'      then toBrowser 'title', b64d getTag 'Project', m
+        when 'ReplyUpdateWsid'    then toBrowser 'title', b64d getTag 'wsid', m
+        when 'ReplyExecute'       then toBrowser 'add', b64d getTag 'result', m
+        when 'ReplyEchoInput'     then toBrowser 'add', b64d(getTag 'input', m) + '\n'
+        when 'ReplyGetLog'        then toBrowser 'set', b64d getTag 'Log', m
+        when 'ReplyAtInputPrompt' then toBrowser 'prompt'
+        when 'ReplyOpenWindow'
+          bs = []; m.replace /<row>(\d+)<\/row><value>1<\/value>/g, (_, l) -> bs.push +l
+          toBrowser 'open', b64d(getTag 'name', m), b64d(getTag 'text', m), +getTag('token', m), +getTag('bugger', m), bs
+        when 'ReplyUpdateWindow'
+          bs = []; m.replace /<row>(\d+)<\/row><value>1<\/value>/g, (_, l) -> bs.push +l
+          toBrowser 'update', b64d(getTag 'name', m), b64d(getTag 'text', m), +getTag('token', m), +getTag('bugger', m), bs
+        when 'ReplyFocusWindow'   then toBrowser 'focus', +getTag 'win', m
+        when 'ReplyCloseWindow'   then toBrowser 'close', +getTag 'win', m
+        when 'ReplyGetAutoComplete'
+          o = b64d getTag 'options', m
+          toBrowser 'autocomplete', +getTag('token', m), +getTag('skip', m), (if o then o.split '\n' else [])
+        when 'ReplyHighlightLine' then toBrowser 'highlight', +getTag('win', m), +getTag 'line', m
+        else log 'unrecognised'; toBrowser 'unrecognised', m
+  return
+
+rm = (a, x) -> i = a.indexOf x; (if i != -1 then a.splice i, 1); return
+
 io.listen(server).on 'connection', (socket) ->
   log 'browser connected'
-
-  toBrowser = (m...) -> log 'to browser: ' + JSON.stringify(m)[..1000]; socket.emit m...
-
-  q = Buffer 0 # a buffer for data received from the server
-  client.on 'data', (data) ->
-    q = Buffer.concat [q, data]
-    while q.length >= 4 and (n = q.readInt32BE 0) <= q.length
-      m = '' + q[8...n]; q = q[n..]
-      log 'from interpreter: ' + JSON.stringify(m)[..1000]
-      if !/^(?:SupportedProtocols|UsingProtocol)=1$/.test m # ignore these
-        switch (/^<(\w+)>/.exec m)?[1] or ''
-          when 'ReplyConnect', 'ReplyNotAtInputPrompt', 'ReplyEdit', 'ReplySaveChanges', 'ReplySetLineAttributes' then ; # ignore
-          when 'ReplyIdentify'      then toBrowser 'title', b64d getTag 'Project', m
-          when 'ReplyUpdateWsid'    then toBrowser 'title', b64d getTag 'wsid', m
-          when 'ReplyExecute'       then toBrowser 'add', b64d getTag 'result', m
-          when 'ReplyEchoInput'     then toBrowser 'add', b64d(getTag 'input', m) + '\n'
-          when 'ReplyGetLog'        then toBrowser 'set', b64d getTag 'Log', m
-          when 'ReplyAtInputPrompt' then toBrowser 'prompt'
-          when 'ReplyOpenWindow'
-            bs = []; m.replace /<row>(\d+)<\/row><value>1<\/value>/g, (_, l) -> bs.push +l
-            toBrowser 'open', b64d(getTag 'name', m), b64d(getTag 'text', m), +getTag('token', m), +getTag('bugger', m), bs
-          when 'ReplyUpdateWindow'
-            bs = []; m.replace /<row>(\d+)<\/row><value>1<\/value>/g, (_, l) -> bs.push +l
-            toBrowser 'update', b64d(getTag 'name', m), b64d(getTag 'text', m), +getTag('token', m), +getTag('bugger', m), bs
-          when 'ReplyFocusWindow'   then toBrowser 'focus', +getTag 'win', m
-          when 'ReplyCloseWindow'   then toBrowser 'close', +getTag 'win', m
-          when 'ReplyGetAutoComplete'
-            o = b64d getTag 'options', m
-            toBrowser 'autocomplete', +getTag('token', m), +getTag('skip', m), (if o then o.split '\n' else [])
-          when 'ReplyHighlightLine' then toBrowser 'highlight', +getTag('win', m), +getTag 'line', m
-          else log 'unrecognised'; toBrowser 'unrecognised', m
-    return
+  sockets.push socket

   onevent = socket.onevent
   socket.onevent = (packet) ->
     log 'from browser: ' + JSON.stringify(packet.data)[..1000]
     onevent.apply socket, [packet]
-
-  cmd = (name, args) -> toInterpreter """<?xml version="1.0" encoding="utf-8"?><Command><cmd>#{name}</cmd><id>0</id><args><#{name}>#{args}</#{name}></args></Command>"""

   socket.on 'exec', (s, trace) -> cmd 'Execute', "<Text>#{b64 s}</Text><Trace>#{+!!trace}</Trace>"
   socket.on 'edit', (text, pos) -> cmd 'Edit', "<Text>#{b64 text}</Text><Pos>#{pos}</Pos><Win>0</Win>"
@@ -164,5 +172,5 @@
   cmd 'Connect', '<Token />'
   cmd 'GetWindowLayout', ''

-  client.on 'end', -> log 'interpreter disconnected'; socket.emit 'end'
-  socket.on 'disconnect', -> log 'browser disconnected'
+  client.on 'end', -> log 'interpreter disconnected'; socket.emit 'end'; rm sockets, socket; return
+  socket.on 'disconnect', -> log 'browser disconnected'; rm sockets, socket; return


Refactoring.

Refactoring.

--- server.coffee
+++ server.coffee
@@ -38,7 +38,16 @@
     oldDocuments = {}
     documents = {}

-    publish._installCallbacks = (computation) ->
+    publish._installCallbacks = ->
+      if Tracker.active
+        computation = Tracker.currentComputation
+      else
+        # Computation can also be passed through current fiber in the case the "added" method is called
+        # from the observeChanges callback from an observeChanges called inside a reactive context.
+        computation = Fiber.current._publishComputation
+
+      return unless computation
+
       unless computation._publishOnStopSet
         computation._publishOnStopSet = true

@@ -72,19 +81,14 @@

         computation._trackerInstance.requireFlush()

+      computation
+
     originalAdded = publish.added
     publish.added = (collectionName, id, fields) ->
       stringId = @_idFilter.idStringify id

-      if Tracker.active
-        currentComputation = Tracker.currentComputation
-      else
-        # Computation can also be passed through current fiber in the case this "added" method is called
-        # from the observeChanges callback from an observeChanges called inside a reactive context.
-        currentComputation = Fiber.current._publishComputation
-      if currentComputation
-        @_installCallbacks currentComputation
-        Meteor._ensure(documents, currentComputation._id, collectionName)[stringId] = true
+      currentComputation = @_installCallbacks()
+      Meteor._ensure(documents, currentComputation._id, collectionName)[stringId] = true if currentComputation

       # If document as already present in publish then we call changed to send updated fields (Meteor sends only a diff).
       # This can hide some errors in publish functions if they one calls "added" on an existing document and we could
@@ -106,12 +110,7 @@

     originalReady = publish.ready
     publish.ready = ->
-      if Tracker.active
-        currentComputation = Tracker.currentComputation
-      else
-        currentComputation = Fiber.current._publishComputation
-      if currentComputation
-        @_installCallbacks currentComputation
+      @_installCallbacks()

       # Mark it as ready only the first time.
       originalReady.call @ unless ready


dont question it

dont question it

--- server.coffee
+++ server.coffee
@@ -7,7 +7,7 @@

 config = require './src/coffee/config'

-HEALTHCHECK_TIMEOUT = 200
+HEALTHCHECK_TIMEOUT = 600

 app = express()


preserve checkbox state in add users dialog

test plan:
* in a course with 2 or more sections, add a
teacher or TA through the 'People' page
* after adding the user(s), click "Add more users"
to start over on the first dialog screen
* confirm that the "Can grade students..." checkbox
has not changed from its previous state

fixes #CNVS-7928

Change-Id: I6fba94f13a32f3d0eee8913529ddeb7dc19355de
Reviewed-on: https://gerrit.instructure.com/24154
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
QA-Review: August Thornton <9adc7a1161ddf32ff608de792a7e50179545f026@instructure.com>
Reviewed-by: Ryan Florence <2d8900489f91ac272fd9ce9d5789db6522668ca2@instructure.com>
Product-Review: Bracken Mosbacker <48b846001bca4166dc3fb7711563afc17525921e@instructure.com>

--- app/coffeescripts/views/courses/roster/CreateUsersView.coffee
+++ app/coffeescripts/views/courses/roster/CreateUsersView.coffee
@@ -79,6 +79,8 @@

     startOverFrd: ->
       @model.startOver()
+      if @model.get('limit_privileges_to_course_section') == "0"
+        @$el.find('#limit_privileges_to_course_section').prop('checked', false)
       @$textarea?.val ''

     afterRender: ->


fix typo

fix typo

--- server.coffee
+++ server.coffee
@@ -455,7 +455,7 @@
     if req.user
       db.collection('users').update {username: req.user.username}, {$set: {lastConnection: new Date()}}, (err) ->
       token = jwt.sign(req.user, config.salt, {expiresInMinutes: 360})
-    res.render('index.jade', { user: req.user, env: 'prod', token: token, version: version})
+    res.render('index.jade', { user: req.user, env: 'prod', token: token, version: app.locals.version})

 # Server
 terminate = () ->


Add some more friendly messages

Add some more friendly messages

- Print a message once we've started up

- Reference the example config file if we're invoked without one

--- server.coffee
+++ server.coffee
@@ -6,7 +6,8 @@

 # Make sure we have the right number of args
 if process.argv.length != 3
-  console.error("Usage: git-tree-srv config_File")
+  console.error("Usage: git-tree-srv config_File\n")
+  console.error("See doc/config.coffee.example in the git-tree-srv package for more information")
   process.exit(1)

 configFile = process.argv[2]
@@ -107,3 +108,5 @@
   )

 ).listen(config.http.port, config.http.bind_address)
+
+console.log("Listening on #{config.http.bind_address}:#{config.http.port}")


Add tests for proper zero downtime deployment

--- test/surveyor.coffee
+++ test/surveyor.coffee
@@ -70,6 +70,99 @@
     clearTimeout model.kill[rand1].one
     model.kill = {}
     done()
+  it 'should not cut over routing while the new commit is not healthy', (done) ->
+    rand1 = Math.floor(Math.random() * (1 << 24)).toString(16)
+    model.slaves = {}
+    model.slaves[rand1] =
+      ip: 'example.com'
+      processes:
+        pidone:
+          status: 'running'
+          commit: '1'
+          repo: 'a'
+        pidtwo:
+          status: 'running'
+          commit: '2'
+          repo: 'a'
+    model.portMap = {}
+    model.portMap[rand1] =
+      pidone:
+        repo: 'a'
+        port: 2014
+        commit: '1'
+    model.manifest =
+      a:
+        instances: 2
+        killable: true
+        opts:
+          commit: '2'
+    model.prevCommits.put 'a', '1', ->
+      surveyor.calculateRoutingTable (err, routes) ->
+        assert.deepEqual err, null
+        assert.deepEqual routes,
+          a:
+            [
+              {
+                host: 'example.com'
+                port: 2014
+              }
+            ]
+        done()
+
+  it 'should cut over routing once the new commit is healthy', (done) ->
+    rand1 = Math.floor(Math.random() * (1 << 24)).toString(16)
+    model.slaves = {}
+    model.slaves[rand1] =
+      ip: 'example.com'
+      processes:
+        pidone:
+          status: 'running'
+          commit: '1'
+          repo: 'a'
+        pidtwo:
+          status: 'running'
+          commit: '2'
+          repo: 'a'
+        pidthree:
+          status: 'running'
+          commit: '2'
+          repo: 'a'
+    model.portMap = {}
+    model.portMap[rand1] =
+      pidone:
+        repo: 'a'
+        port: 2014
+        commit: '1'
+      pidtwo:
+        repo: 'a'
+        port: 2015
+        commit: '2'
+      pidthree:
+        repo: 'a'
+        port: 2016
+        commit: '2'
+    model.manifest =
+      a:
+        instances: 2
+        killable: true
+        opts:
+          commit: '2'
+    model.prevCommits.put 'a', '1', -> #FIXME Decide whether this should be checked by router or whether something else should remove the prevCommit
+      surveyor.calculateRoutingTable (err, routes) ->
+        assert.deepEqual err, null
+        assert.deepEqual routes,
+          a:
+            [
+              {
+                host: 'example.com'
+                port: 2015
+              }
+              {
+                host: 'example.com'
+                port: 2016
+              }
+            ]
+        done()
   it 'should respect the kill ttl set in the options', (done) ->
     rand1 = Math.floor(Math.random() * (1 << 24)).toString(16)
     model.slaves[rand1] =


Make sure that iframe state is not saved before it's loaded

[#89024392]

--- app/assets/javascripts/iframe-saver.coffee
+++ app/assets/javascripts/iframe-saver.coffee
@@ -28,10 +28,6 @@
     if (@put_url or @get_url)
       IFrameSaver.instances.push @

-    model_did_load = () =>
-      if @get_url
-        @load_interactive()
-
     phone_answered = () =>
       @iframePhone.addListener 'setLearnerUrl', (learner_url) =>
         @learner_url = learner_url
@@ -57,14 +53,13 @@
             else
               @$delete_button.hide()

-      if @put_url
-        @set_autosave_enabled(true)
-
       @iframePhone.post('getExtendedSupport')
       @iframePhone.post('getLearnerUrl')
-      # TODO: There used to be a callback for model_did_load
-      # hopefully this will work
-      model_did_load()
+
+      # Enable autosave after model is loaded. Theoretically we could save empty model before it's loaded,
+      # so its state would be lost.
+      @load_interactive =>
+        @set_autosave_enabled(true)

     @iframePhone = new iframePhone.ParentEndpoint($iframe[0], phone_answered)

@@ -128,8 +123,11 @@
       error: (jqxhr, status, error) =>
         @error("couldn't save interactive")

-  load_interactive: ->
-    return unless @get_url
+  load_interactive: (callback) ->
+    unless @get_url
+      callback()
+      return
+
     $.ajax
       url: @get_url
       success: (response) =>
@@ -141,8 +139,11 @@
             @$delete_button.show() if @should_show_delete == null or @should_show_delete
       error: (jqxhr, status, error) =>
         @error("couldn't load interactive")
+      complete: =>
+        callback()

   set_autosave_enabled: (v) ->
+    return unless @put_url
     # Save interactive every 5 seconds, on window focus and iframe mouseout just to be safe.
     # Focus event is attached to the window, so it has to have unique namespace. Mouseout is attached to the iframe
     # itself, but other code can use that event too (e.g. logging).


Fix IframeSaver bug preventing user from going to the next page

--- app/assets/javascripts/iframe-saver.coffee
+++ app/assets/javascripts/iframe-saver.coffee
@@ -99,8 +99,6 @@

   save_to_server: (interactive_json, learner_url) ->
     return unless @put_url
-    # Do not send the same state to server over and over again.
-    return if JSON.stringify(interactive_json) == JSON.stringify(@saved_state)

     runSuccess = =>
       @saved_state = interactive_json
@@ -109,7 +107,9 @@
       else
         @default_success

-    if interactive_json is "nochange"
+    # Do not send the same state to server over and over again.
+    # "nochange" is a special type of response from CODAP.
+    if JSON.stringify(interactive_json) == JSON.stringify(@saved_state) || interactive_json is "nochange"
       runSuccess()
       return


Show activity on permission create

Show activity on permission create

--- app/assets/rdb/PermissionComponents.coffee
+++ app/assets/rdb/PermissionComponents.coffee
@@ -82,10 +82,15 @@
     principal = @refs['principal'].value()
     role      = @refs['role'].value().value

-    @props.collection.create
-      role: role,
-      principal: principal
-    @props.collection.fetch merge: true
+    p = new Promise (resolve, reject) =>
+      @props.collection.create {role: role, principal: principal},
+        success: resolve
+        error: reject
+
+    p = p.then =>
+      @props.collection.fetch merge: true
+
+    @refs['indicator'].track p

     @refs['principal'].clear()
     @refs['principal'].focus()


[backport] Restored setEcAction method

[backport] Restored setEcAction method

--- app/coffee/modules/common/analytics.coffee
+++ app/coffee/modules/common/analytics.coffee
@@ -155,4 +155,10 @@
         })
         @win.ga('send', 'event', 'checkout', 'Plan checkout')

+    setEcAction: (action, page) ->
+        @win.ga('ec:setAction', action, {
+            'list': page
+        })
+        @.trackEvent("ecommerce", "set-action", (action+page), 0)
+
 module.service("$tgAnalytics", AnalyticsService)


Updated comments

Updated comments

--- src/webpipe.coffee
+++ src/webpipe.coffee
@@ -1,6 +1,4 @@
-# webpipe.js
-
-# webpipe.js can be used both on the server, as a command-line interface, or to
+# webpipe.js can be used on the server, as a command-line interface, or
 # directly in the browser. The `webpipe` object is automagically added to your
 # environment. No need for `new` or any other sort of initialization.


fix(wrapper): ensure eventbus is online when removing a handler

Fixes #48 #52

--- src/wrapper.coffee
+++ src/wrapper.coffee
@@ -161,7 +161,9 @@
           deconstructor.displayName = "#{CONSTANTS.MODULE}/#{CONSTANTS.COMPONENT}: EventBusStub.registerHandler (deconstructor)"
           return deconstructor
         unregisterHandler: (address, handler) ->
-          eventBus.unregisterHandler(address, handler)
+          # the handler is only valid when connected
+          if eventBus.readyState() is EventBusStub.EventBus.OPEN
+            eventBus.unregisterHandler(address, handler)
         readyState: ->
           eventBus.readyState()
         ### expose current used internal instance of actual EventBus ###


Fix doc stubs for up.viewport

Fix doc stubs for up.viewport

--- lib/assets/javascripts/up/viewport.js.coffee
+++ lib/assets/javascripts/up/viewport.js.coffee
@@ -35,7 +35,7 @@
   @param {Number} scrollPos
   @param {String}[options.duration]
   @param {String}[options.easing]
-  @returns {Deferred}
+  @return {Deferred}
   @protected
   ###
   scroll = (viewOrSelector, scrollPos, options) ->
@@ -83,7 +83,7 @@
   @param {Number} [options.duration]
   @param {String} [options.easing]
   @param {Number} [options.padding]
-  @returns {Deferred}
+  @return {Deferred}
   @protected
   ###
   reveal = (elementOrSelector, options) ->


WIP text tool bad mark button

WIP text tool bad mark button

--- app/assets/javascripts/components/transcribe/tools/text-tool/index.cjsx
+++ app/assets/javascripts/components/transcribe/tools/text-tool/index.cjsx
@@ -146,6 +146,10 @@
     if [13].indexOf(e.keyCode) >= 0 # ENTER
       @commitAnnotation()
       e.preventDefault()
+
+  handleBadMark: ()->
+    newAnnotation = []
+    newAnnotation["low_quality_subject"]

   render: ->


Send pay payment information to payment engines

--- app/assets/javascripts/neighborly/projects/contributions/edit.js.coffee
+++ app/assets/javascripts/neighborly/projects/contributions/edit.js.coffee
@@ -22,12 +22,12 @@
       initialize: ->
         _.bindAll this, 'showContent'
         this.$('.methods input').click this.showContent
-        $('.create-contribution-page #pay_payment_fees').on 'change', this.showTotalValue
+        $('.create-contribution-page #pay_payment_fees').on 'change', this.togglePaymentFee
         this.$('.methods input:first').click()
-        this.showTotalValue()
+        this.togglePaymentFee()

       showContent: (e)->
-        this.showTotalValue(e)
+        this.togglePaymentFee()
         this.$('.payment-method').addClass('loading-section')
         $payment = $("##{$(e.currentTarget).val()}-payment.payment-method")

@@ -39,12 +39,14 @@
             $payment.show()
             this.$('.payment-method').removeClass('loading-section')

-      showTotalValue: =>
+      togglePaymentFee: =>
         $input = $('.create-contribution-page header .total-value input')
         $target = this.$('.methods input:checked')
         if $('.create-contribution-page #pay_payment_fees').is(':checked')
           value = $($target).data('value-with-fees')
+          $('[data-pay-payment-fee]').val('1')
         else
           value = $($target).data('value-without-fees')
+          $('[data-pay-payment-fee]').val('0')

         $input.val("#{$input.data('total-text')} #{value}") if value


number of pages calculation fix

number of pages calculation fix

--- src/pagination/coffee/services/pagination.coffee
+++ src/pagination/coffee/services/pagination.coffee
@@ -40,7 +40,7 @@
        * @function
       ###
       getNumberOfPages: (size, count) ->
-        pages = parseInt(count / size)
+        pages = Math.ceil count / size
         if pages is 0
           pages+1
         pages


missing space in an info banner

missing space in an info banner

--- src/smc-webapp/r_misc/old-coffee-code-index.cjsx
+++ src/smc-webapp/r_misc/old-coffee-code-index.cjsx
@@ -846,8 +846,7 @@
             <Space />
             Projects running on free servers compete for resources with a large number of other free projects.
             The free servers are randomly rebooted frequently,
-            and are often much more heavily loaded than members-only servers.
-            {suggestion}
+            and are often much more heavily loaded than members-only servers. {suggestion}

     </Alert>


Index into array instead of removing matchers

--- src/app/text-mate-scope-selector-matchers.coffee
+++ src/app/text-mate-scope-selector-matchers.coffee
@@ -32,9 +32,10 @@
     @matchers.push(matcher[1]) for matcher in others

   matches: (scopes) ->
-    matcher = @matchers.shift()
+    index = 0
+    matcher = @matchers[index]
     for scope in scopes
-      matcher = @matchers.shift() if matcher.matches(scope)
+      matcher = @matchers[++index] if matcher.matches(scope)
       return true unless matcher?
     false


Mark matchers as internal

Mark matchers as internal

--- src/app/text-mate-scope-selector-matchers.coffee
+++ src/app/text-mate-scope-selector-matchers.coffee
@@ -1,3 +1,7 @@
+###
+# Internal #
+###
+
 class SegmentMatcher
   constructor: (segment) ->
     @segment = segment.join('')


missing saving

missing saving

--- layout/Bradesco/ArquivoTrailing.coffee
+++ layout/Bradesco/ArquivoTrailing.coffee
@@ -48,6 +48,14 @@
         type: 'numeric'
     }
     {
+        field: 'qtde_registros'
+        startPos: 24
+        endPos: 29
+        length: 6
+        required: true
+        type: 'numeric'
+    }
+    {
         field: 'filler'
         startPos: 30
         endPos: 240


logger: Rename log() to msg()

logger: Rename log() to msg()

To avoid confusion with console.

--- logger.coffee
+++ logger.coffee
@@ -38,7 +38,7 @@
     if activeTags[tag]?
       api.print str, raw

-  log: (level, str, raw=false) ->
+  msg: (level, str, raw=false) ->
     if level >= logLevel
       api.print str, raw

@@ -53,19 +53,19 @@
     return logLevel

   debug: (str, raw=false) ->
-    api.log api.LEVEL_DEBUG, str, raw
+    api.msg api.LEVEL_DEBUG, str, raw

   info: (str, raw=false) ->
-    api.log api.LEVEL_INFO, str, raw
+    api.msg api.LEVEL_INFO, str, raw

   warn: (str, raw=false) ->
-    api.log api.LEVEL_WARN, str, raw
+    api.msg api.LEVEL_WARN, str, raw

   error: (str, raw=false) ->
-    api.log api.LEVEL_ERROR, str, raw
+    api.msg api.LEVEL_ERROR, str, raw

   fatal: (str, raw=false) ->
-    api.log api.LEVEL_FATAL, str, raw
+    api.msg api.LEVEL_FATAL, str, raw

 logLevel = api.LEVEL_DEBUG


Extract handle events helper

Extract handle events helper

--- src/buffered-process.coffee
+++ src/buffered-process.coffee
@@ -72,37 +72,9 @@
       @process = @spawn(@getCmdPath(), cmdArgs, cmdOptions)
     else
       @process = @spawn(command, args, options)
+
     @killed = false
-
-    stdoutClosed = true
-    stderrClosed = true
-    processExited = true
-    exitCode = 0
-    triggerExitCallback = ->
-      return if @killed
-      if stdoutClosed and stderrClosed and processExited
-        exit?(exitCode)
-
-    if stdout
-      stdoutClosed = false
-      @bufferStream @process.stdout, stdout, ->
-        stdoutClosed = true
-        triggerExitCallback()
-
-    if stderr
-      stderrClosed = false
-      @bufferStream @process.stderr, stderr, ->
-        stderrClosed = true
-        triggerExitCallback()
-
-    if exit
-      processExited = false
-      @process.on 'exit', (code) ->
-        exitCode = code
-        processExited = true
-        triggerExitCallback()
-
-    @process.on 'error', (error) => @handleError(error)
+    @handeEvents(stdout, stderr, exit) if @process?

   ###
   Section: Event Subscription
@@ -220,6 +192,38 @@
       process.nextTick => @handleError(spawnError)
     process

+  handleEvents: (stdout, stderr, exit) ->
+    stdoutClosed = true
+    stderrClosed = true
+    processExited = true
+    exitCode = 0
+    triggerExitCallback = ->
+      return if @killed
+      if stdoutClosed and stderrClosed and processExited
+        exit?(exitCode)
+
+    if stdout
+      stdoutClosed = false
+      @bufferStream @process.stdout, stdout, ->
+        stdoutClosed = true
+        triggerExitCallback()
+
+    if stderr
+      stderrClosed = false
+      @bufferStream @process.stderr, stderr, ->
+        stderrClosed = true
+        triggerExitCallback()
+
+    if exit
+      processExited = false
+      @process.on 'exit', (code) ->
+        exitCode = code
+        processExited = true
+        triggerExitCallback()
+
+    @process.on 'error', (error) => @handleError(error)
+    return
+
   handleError: (error) ->
     handled = false
     handle = -> handled = true


Avatararea: initial take on sidebar

Avatararea: initial take on sidebar

--- client/Main/avatararea/avatararea.coffee
+++ client/Main/avatararea/avatararea.coffee
@@ -11,16 +11,14 @@
     account = @getData()

     @avatar = new AvatarView
-      tagName    : "div"
-      cssClass   : "avatar-image-wrapper"
+      tagName    : 'div'
+      cssClass   : 'avatar-image-wrapper'
       attributes :
-        title    : "View your public profile"
+        title    : 'View your public profile'
       size       :
-        width    : 25
-        height   : 25
+        width    : 35
+        height   : 35
     , account
-
-    @profileLink = new ProfileLinkView {}, account

     @groupSwitcherPopup = new AvatarPopupGroupSwitcher
       cssClass : "group-switcher"
@@ -45,12 +43,14 @@
       @groupSwitcherPopup.populateGroups()
       @groupSwitcherPopup.populatePendingGroups()

+
   pistachio: ->
+
+    {profile} = @getData()
+
     """
     {{> @avatar}}
-

-
{{> @profileLink}}

-
@{{#(profile.nickname)}}

-      {{> @groupsSwitcherIcon}}
-

-    """+    #{profile.firstName}
+    {{> @groupsSwitcherIcon}}
+    """
+


remove suffix download=1 : seems to be broker (serverSide)

--- client/app/components/file_picker.coffee
+++ client/app/components/file_picker.coffee
@@ -68,7 +68,7 @@
         @props.valueLink.requestChange files

     displayFile: (file) ->
-        if (url = getFileURL file) window.open url + '?download=1'
+        if (url = getFileURL file) window.open url
         else console.error "broken file : ", file

     render: ->


Use appropriate scope for embedded Pic source

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -2161,7 +2161,7 @@
 			# .PS/.PE: Pic
 			begin: "^([.'])[ \t]*(PS)\\b([\\d \t]*(?:#.*)?)?(\\\\[#\"].*)?$"
 			end:   "^([.'])[ \t]*(P[EF])\\b"
-			contentName: "markup.other.graphics.preprocessor.pic.roff"
+			contentName: "source.embedded.pic"
 			patterns: [include: "source.pic"]
 			beginCaptures:
 				0: {name: "meta.function.begin.picture.section.macro.roff"}


simplify passing passing partial grunt config into BumbleBuild.gruntConfig

--- gruntConfig.coffee
+++ gruntConfig.coffee
@@ -22,7 +22,7 @@
   pkg = grunt.file.readJSON("package.json")

   # configuration
-  grunt.initConfig
+  _.defaultsDeep initConfig,
     pkg: pkg

     # args to initConfig method are the tasks
@@ -37,14 +37,11 @@
       options:
         shorthandCompacting: false,
         keepBreaks: true
-
       distrib:
         files:
           "dist/#{pkg.name}.css": [
             'css/**/*.css'
           ]
-
-
     shell:
       buildExamples:
         command: bumbleScriptCommand('buildExamples.coffee')
@@ -90,6 +87,7 @@
       distrib: require("./webpack.config.coffee")
       optimize: require("./webpack.optimized.config.coffee")

+  grunt.initConfig initConfig

   # tasks
   grunt.registerTask 'test', ["shell:test"]


Teamwork: Introducing pane launcher for Teamwork.

--- client/app/Applications/Teamwork.kdapplication/Views/teamworkapp.coffee
+++ client/app/Applications/Teamwork.kdapplication/Views/teamworkapp.coffee
@@ -13,7 +13,6 @@
       shareSessionKeyInfo : "
This is your session key, you can share this key with your friends to work together.
"
       firebaseInstance    : instanceName
       sessionKey          : @getOptions().sessionKey
-      enableChat          : yes
       panelClass          : TeamworkPanel
       delegate            : this
       panels              : [
@@ -26,6 +25,7 @@
             callback      : (panel, workspace) => @showToolsModal panel, workspace
           }
         ]
+        floatingPanes     : [ "chat" , "terminal", "preview" ]
         layout            :
           direction       : "vertical"
           sizes           : [ "25%", null ]
@@ -36,21 +36,8 @@
               name        : "finder"
             }
             {
-              type        : "split"
-              options     :
-                direction : "horizontal"
-                sizes     : [ "50%", null ]
-                splitName : "InnerSplit"
-              views       : [
-                {
-                  type    : "tabbedEditor"
-                  name    : "editor"
-                }
-                {
-                  type    : "terminal"
-                  name    : "terminal"
-                }
-              ]
+              type        : "tabbedEditor"
+              name        : "editor"
             }
           ]
       ]


Get constructor width options

Get constructor width options

--- framer/TextLayer.coffee
+++ framer/TextLayer.coffee
@@ -15,7 +15,8 @@
 		super options

 		# Set default width
-		@width = if @parent? then @parent.width else Screen.width
+		if not options.width
+			@width = if @parent? then @parent.width else Screen.width

 		# Set type defaults
 		if not @fontFamily


React to account disables

React to account disables

--- assets/javascripts/app.coffee
+++ assets/javascripts/app.coffee
@@ -30,6 +30,13 @@
     delete window.posts
     delete window.unreads
     $("#bootstrap").remove()
+    @user.on "backend:update", (model) =>
+      if model.id is @user.id and model.get("disabled")
+        alert "Yhteytesi on lukittu! Sinut kirjataan ulos. Ota yhteys pelinjohtoon."
+        setTimeout ->
+          window.location = "/logout"
+        , 5000
+
   App.addInitializer (options) ->
     User = require("./models/user.coffee")
     Users = require("./collections/users.coffee")


fix(focus): Don't clear when item is scrolled out of retained range

--- internal_packages/thread-list/lib/thread-list-store.coffee
+++ internal_packages/thread-list/lib/thread-list-store.coffee
@@ -48,28 +48,43 @@
     @createListDataSource()

   _onDataChanged: ({previous, next} = {}) =>
+    # This code keeps the focus and keyboard cursor in sync with the thread list.
+    # When the thread list changes, it looks to see if the focused thread is gone,
+    # or no longer matches the query criteria and advances the focus to the next
+    # thread.
+
+    # This means that removing a thread from view in any way causes selection
+    # to advance to the adjacent thread. Nice and declarative.
+
     if previous and next
-      focusedId = FocusedContentStore.focusedId('thread')
-      keyboardId = FocusedContentStore.keyboardCursorId('thread')
+      focused = FocusedContentStore.focused('thread')
+      keyboard = FocusedContentStore.keyboardCursor('thread')
       viewModeAutofocuses = WorkspaceStore.layoutMode() is 'split' or WorkspaceStore.topSheet().root is true
+      matchers = next.query()?.matchers()

-      focusedIndex = previous.offsetOfId(focusedId)
-      keyboardIndex = previous.offsetOfId(keyboardId)
+      focusedIndex = if focused then previous.offsetOfId(focused.id) else -1
+      keyboardIndex = if keyboard then previous.offsetOfId(keyboard.id) else -1

-      shiftIndex = (i) =>
+      nextItemFromIndex = (i) =>
         if i > 0 and (next.modelAtOffset(i - 1)?.unread or i >= next.count())
-          return i - 1
+          nextIndex = i - 1
         else
-          return i
+          nextIndex = i

-      focusedLost = focusedIndex >= 0 and next.offsetOfId(focusedId) is -1
-      keyboardLost = keyboardIndex >= 0 and next.offsetOfId(keyboardId) is -1
+        # May return null if no thread is loaded at the next index
+        next.modelAtOffset(nextIndex)

-      if viewModeAutofocuses and focusedLost
-        Actions.setFocus(collection: 'thread', item: next.modelAtOffset(shiftIndex(focusedIndex)))
+      notInSet = (model) ->
+        if matchers
+          return model.matches(matchers) is false
+        else
+          return next.offsetOfId(model.id) is -1

-      if keyboardLost
-        Actions.setCursorPosition(collection: 'thread', item: next.modelAtOffset(shiftIndex(keyboardIndex)))
+      if viewModeAutofocuses and focused and notInSet(focused)
+        Actions.setFocus(collection: 'thread', item: nextItemFromIndex(focusedIndex))
+
+      if keyboard and notInSet(keyboard)
+        Actions.setCursorPosition(collection: 'thread', item: nextItemFromIndex(keyboardIndex))

     @trigger(@)


Hotfix for Activity page

Hotfix for Activity page

--- client/app/Applications/Activity.kdapplication/AppView.coffee
+++ client/app/Applications/Activity.kdapplication/AppView.coffee
@@ -4,7 +4,7 @@

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

-    options.cssClass   = "content-page activity fixed"
+    options.cssClass   = "content-page activity"
     options.domId      = "content-page-activity"

     super options, data
@@ -14,6 +14,10 @@
   viewAppended:->

     {entryPoint}      = KD.config
+
+    if entryPoint in ['koding', 'guest']
+      @setClass 'fixed'
+
     HomeKonstructor   = if entryPoint and entryPoint.type isnt 'profile' then GroupHomeView else KDCustomHTMLView
     @feedWrapper      = new ActivityListContainer
     @innerNav         = new ActivityInnerNavigation cssClass : 'fl'


Fixed whitespaces

Fixed whitespaces

--- client/app/lib/providers/environmentsmachinestatemodal.coffee
+++ client/app/lib/providers/environmentsmachinestatemodal.coffee
@@ -115,7 +115,7 @@
         if /NetworkOut/i.test error

           paymentController.subscriptions (err, subscription) =>
-
+
             if subscription.planTitle is 'free'
               @customErrorMessage = "
                 <p>You've reached your outbound network usage


optimize js call

optimize js call

--- BackofficeBundle/Resources/public/coffee/addPrototype.coffee
+++ BackofficeBundle/Resources/public/coffee/addPrototype.coffee
@@ -35,7 +35,7 @@

   @toogleAddButton()

-  if @collectionHolder.children().length == 0 && @settings.required
+  if @getIndex() == 0 && @settings.required
     @addPrototype()

   return


everything working, class'd so we don't have variables processing everywhere

--- src/scripts/hubot-variables.coffee
+++ src/scripts/hubot-variables.coffee
@@ -26,34 +26,40 @@

 module.exports = (robot) ->
   variableRE = /(\\)?(?:\$([a-zA-Z_]\w+)|\${([a-zA-Z_]\w+)})/g
-  can_edit_var = (user, varname) ->
-    variable = robot.brain.data.variables[varname]
-    if user.roles
-      if "edit_variables" in user.roles
-        return true
-      if "edit_variable_" + varname in user.roles
-        return true
-    return !!variable.readonly

+  class Variables
+    @can_edit_var = (user, varname) ->
+      variable = robot.brain.data.variables[varname]
+      if user.roles
+        if "edit_variables" in user.roles
+          return true
+        if "edit_variable_" + varname in user.roles
+          return true
+      return !!variable.readonly
+    @process = (string, user) ->
+      return string.replace variableRE, ($0, $1, $2, $3) ->
+        if $1
+          return $0
+        varname = $2 || $3
+        if varname == "who"
+          return user.name
+        v = robot.brain.data.variables[varname]
+        if !v
+          return $0
+        return v.values[Math.floor(Math.random() * v.values.length)]
+
+  robot.variables = Variables
   if process.env.ALWAYS_VARIABLE
     robot.adapter._oldsend = robot.adapter.send
     robot.adapter.send = (envelope, strings...) ->
       for i in [0...strings.length]
-        strings[i] = strings[i].replace variableRE, ($0, $1, $2, $3) ->
-          if $1
-            return $0
-          varname = $2 || $3
-          if !robot.brain.data.variables[varname]
-            return $0
-          index = robot.brain.data.variables.values[Math.floor(Math.random() * robot.brain.data.variables.values.length)]
-          return robot.brain.data.variables[varname].values[index]
+        strings[i] = robot.variables.process strings[i], envelope.user
       @_oldsend envelope, strings
-

   # constructor
   robot.brain.data.variables = {}

-  robot.respond /create var (\w+)$/, (msg) ->
+  robot.hear /^create var (\w+)$/, (msg) ->
     varname = msg.match[1]
     if robot.brain.data.variables[varname]
       msg.reply "Sorry, Variable of '" + varname + "' already exists."
@@ -63,7 +69,7 @@
     }
     msg.reply "Okay."

-  robot.respond /remove var (\w+)\s*(!+)?$/, (msg) ->
+  robot.hear /^remove var (\w+)\s*(!+)?$/, (msg) ->
     varname = msg.match[1]
     is_forced = !!msg.match[2]
     if !robot.brain.data.variables[varname]
@@ -78,7 +84,7 @@
       msg.reply "Okay, removed variable " + varname
     delete robot.brain.data.variables[varname]

-  robot.respond /add value (\w+) (.*)$/, (msg) ->
+  robot.hear /^add value (\w+) (.*)$/, (msg) ->
     varname = msg.match[1]
     value = msg.match[2]
     lcvalue = value.toLowerCase()
@@ -86,7 +92,7 @@
     if !robot.brain.data.variables[varname]
       msg.reply "Sorry, I don't know of a variable '" + varname + "'."
       return
-    if !can_edit_var msg.message.user, varname
+    if !robot.variables.can_edit_var msg.message.user, varname
       msg.reply "Sorry, you don't have permissions to edit '"+varname+"'."
       return
     if value.match(variableRE)
@@ -99,19 +105,19 @@
     robot.brain.data.variables[varname].values.push value
     msg.reply "Okay."

-  robot.respond /remove value (\w+) (.*)$/, (msg) ->
+  robot.hear /^remove value (\w+) (.*)$/, (msg) ->
     varname = msg.match[1]
     value = msg.match[2]
     if !robot.brain.data.variables[varname]
       msg.reply "Sorry, I don't know of a variable '" + varname + "'."
       return
-    if !can_edit_var msg.message.user, varname
+    if !robot.variables.can_edit_var msg.message.user, varname
       msg.reply "Sorry, you don't have permissions to edit '"+varname+"'."
       return
     robot.brain.data.variables[varname].values.remove value
     msg.reply "Okay."

-  robot.respond /var (\w+) type (var|verb|noun)$/, (msg) ->
+  robot.hear /^var (\w+) type (var|verb|noun)$/, (msg) ->
     varname = msg.match[1]
     if !robot.brain.data.variables[varname]
       msg.reply "Sorry, I don't know of a variable '" + varname + "'."
@@ -119,14 +125,14 @@
     robot.brain.data.variables[varname].type = msg.match[2]
     msg.reply "Okay."

-  robot.respond /list var (\w+)$/, (msg) ->
+  robot.hear /^list var (\w+)$/, (msg) ->
     varname = msg.match[1]
     if !robot.brain.data.variables[varname]
       msg.reply "Sorry, I don't know of a variable '" + varname + "'."
       return
     msg.reply robot.brain.data.variables[varname].values.join(', ')

-  robot.respond /list vars$/, (msg) ->
+  robot.hear /^list vars$/, (msg) ->
     ret = []
     for varname in Object.keys(robot.brain.data.variables)
       v = robot.brain.data.variables[varname]


fix base64 img bug

fix base64 img bug

--- lib/md.coffee
+++ lib/md.coffee
@@ -484,6 +484,7 @@
         src.startsWith('https://') or
         src.startsWith('atom://')  or
         src.startsWith('file://')  or
+        src.startsWith('data:image/') or
         src[0] == '#')) and
       (src.startsWith('./') or
         src.startsWith('../') or


LayerScanner: Break after match

LayerScanner: Break after match

--- lib/assets/javascripts/unpoly/classes/layer_scanner.coffee
+++ lib/assets/javascripts/unpoly/classes/layer_scanner.coffee
@@ -26,21 +26,21 @@
       return solutions

     if selector == ':main'
-      solutions = @getLayerMains()
+      solutions = @getLayerMains() # could be one solution

     else if selector == ':zone'
       if zone = @getOriginZone()
-        solutions = [zone]
+        solutions = [zone] # is one solution

     else if match = selector.match(/^\:zone (.+)$/)
       zoneDescendantSelector = match[1]
       if (zoneSolution = @originZone()) && (zoneDescendantMatch = up.fragment.get(zoneSolution.element, zoneDescendantSelector))
         descendantSolution = new up.TargetSolution(zoneDescendantSelector, zoneDescendantMatch)
         # In this branch the user wants the zone to be part of the selector.
-        solutions = [descendantSolution.within(zoneSolution)]
+        solutions = [descendantSolution.within(zoneSolution)] # is one solution

     else if selector == ':closest-zone'
-      solutions = @getOriginZones()
+      solutions = @getOriginZones() # must be multiple solutions :(

     else if match = selector.match(/^\:closest-zone (.+)$/)
       for zoneSolution in @getOriginZones()
@@ -48,14 +48,15 @@
           descendantSolution = new up.TargetSolution(zoneDescendantSelector, zoneDescendantMatch)
           # In this branch the user wants the zone to be part of the selector.
           solutions = [descendantSolution.within(zoneSolution)]
+          break

     else
       # Now we have a regular selector like ".foo"

       solutions = []

+      # If we have an @origin we can be smarter about finding oldElement.
       if @isOriginLayerUpdate()
-        # If we have an @origin we can be smarter about finding oldElement.
         # First, we check if @origin itself or one of its ancestors would match.
         if closestMatchInLayer = up.fragment.closest(@origin, selector)
           solutions.push(new up.TargetSolution(selector, closestMatchInLayer))
@@ -64,14 +65,10 @@
         for zone in @getOriginZones()
           if matchInZone = up.fragment.subtree(zone.element, selector)[0]
             solutions.push(new up.TargetSolution(selector, matchInZone))
+            break

       if firstMatchInLayer = up.fragment.get(selector, { @layer })
         solutions.push(new up.TargetSolution(selector, firstMatchInLayer))
-
-        solutions.push({
-          element: firstMatchInLayer,
-          selector: up.fragment.improveTarget(selector, firstMatchInLayer)
-        })

     # Cannot place <body> in an overlay
     if @layer.isOverlay() || @layer == 'new'