Start of tags, some more derived fields

Start of tags, some more derived fields

--- src/javascript/character_stats.coffee
+++ src/javascript/character_stats.coffee
@@ -7,537 +7,727 @@
   calcDerived: ->
     @combatKillsTotal = @combatKillsHighSec + @combatKillsLowSec + @combatKillsNullSec + @combatKillsNullSec
     @combatDeathsTotal = @combatDeathsHighSec + @combatDeathsLowSec + @combatDeathsNullSec + @combatDeathsNullSec
+    @iskSpentPercent = 100 * (@iskOut / @iskIn)
+    @kdRatio = @combatKillsTotal / @combatDeathsTotal
+    @averageSessionLength = @characterMinutes / @characterSessionsStarted

   metadata:
     daysOfActivity:
       name: "Active Days"
       description: "The number of days in the period with recorded activity"
+      tags: ['general']
     characterMinutes:
       name: "Minutes Logged In"
       description: "The number of minutes logged in as this character"
+      tags: ['general']
     characterSessionsStarted:
       name: "Logins"
       description: "The number of times the character has logged into the game"
+      tags: ['general']
     iskIn:
       name: "ISK Earned"
       description: "The total amount of positive ISK transactions in the character wallet"
+      tags: ['general']
     iskOut:
       name: "ISK Spent"
       description: "The total amount of negative ISK transactions in the character wallet"
+      tags: ['general']
     travelJumpsStargateHighSec:
       name: "High Sec Jumps"
       description: "The number of jumps through a gate in high security space"
+      tags: ['travel']
     travelJumpsStargateLowSec:
       name: "Low Sec Jumps"
       description: "The number of jumps through a gate in low security space"
+      tags: ['travel']
     travelJumpsStargateNullSec:
       name: "Null Sec Jumps"
       description: "The number of jumps through a gate in null security space"
+      tags: ['travel']
     travelJumpsWormhole:
       name: "Wormhole Jumps"
       description: "The number of jumps through a wormhole"
+      tags: ['travel']
     travelDocksHighSec:
       name: "Docked in High Sec"
       description: "The number of docks in high security space"
+      tags: ['travel']
     travelDocksLowSec:
       name: "Docked in Low Sec"
       description: "The number of docks in low security space"
+      tags: ['travel']
     travelDocksNullSec:
       name: "Docked in Null Sec"
       description: "The number of docks in null security space"
+      tags: ['travel']
     pveMissionsSucceeded:
       name: "Missions Completed"
       description: "The number of missions completed"
+      tags: ['pve']
     pveMissionsSucceededEpicArc:
       name: "Epic Arcs Completed"
       description: "The number of epic arc missions completed"
+      tags: ['pve']
     combatKillsHighSec:
       name: "High Sec Kills"
       description: "The number of kills in high security space"
+      tags: ['pvp']
     combatKillsLowSec:
       name: "Low Sec Kills"
       description: "The number of kills in low security space"
+      tags: ['pvp']
     combatKillsNullSec:
       name: "Null Sec Kills"
       description: "The number of kills in null security space"
+      tags: ['pvp']
     combatKillsWormhole:
       name: "Wormhole Kills"
       description: "The number of kills in wormhole space"
+      tags: ['pvp']
     combatKillsPodTotal:
       name: "Pods Killed"
       description: "The total number of capsule kills (final blows)"
+      tags: ['pvp']
     combatDeathsHighSec:
       name: "High Sec Deaths"
       description: "The number of deaths in high security space"
+      tags: ['deaths']
     combatDeathsLowSec:
       name: "Low Sec Deaths"
       description: "The number of deaths in low security space"
+      tags: ['deaths']
     combatDeathsNullSec:
       name: "Null Sec Deaths"
       description: "The number of deaths in null security space"
+      tags: ['deaths']
     combatDeathsWormhole:
       name: "Wormhole Deaths"
       description: "The number of deaths in wormhole space"
+      tags: ['deaths']
     combatDeathsPodTotal:
       name: "Times Podded"
       description: "The total number of capsule deaths (times podded)"
+      tags: ['deaths']
     combatKillsAssists:
       name: "Assists"
       description: "The total number of kill assists"
+      tags: ['pvp']
     combatCapDrainedbyPC:
       name: "Cap Drained by Players"
       description: "The amount of the characters capacitor that has been drained by other player"
+      tags: ['pvp']
     combatCapDrainingPC:
       name: "Cap Drained from Players"
       description: "The amount of capacitor the character has drained from other players"
+      tags: ['pvp']
     combatCriminalFlagSet:
       name: "Times Criminal Flagged"
       description: "The number of times the character has had the crimewatch criminal flag set"
+      tags: ['pvp']
     combatDamageFromNPCsAmount:
       name: "Damage Taken from NPCs"
       description: "The total amount of damage taken from NPCs."
+      tags: ['pve']
     combatDamageFromNPCsNumShots:
       name: "Times Shot by NPCs"
       description: "The total number of times NPCs have shot the character."
+      tags: ['pve']
     combatDamageFromPlayersBombAmount:
       name: "Damage Taken from Bombs"
       description: "The total damage taken from bombs"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersBombNumShots:
       name: "Hits Taken from Bombs"
       description: "The total number of hits taken from bombs"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersEnergyAmount:
       name: "Damage Taken from Lasers"
       description: "The total damage taken from laser turrets"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersEnergyNumShots:
       name: "Hits Taken from Lasers"
       description: "The total number of hits taken from laser turrets"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersFighterMissileAmount:
       name: "Damage Taken from Fighter Bombers"
       description: "The total damage taken from fighter bombers"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersFighterMissileNumShots:
       name: "Hits Taken from Fighter Bombers"
       description: "The total number of hits taken from fighter bombers"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersHybridAmount:
       name: "Damage Taken from Hybrids"
       description: "The total damage taken from hybrid turrets"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersHybridNumShots:
       name: "Hits Taken from Hybrids"
       description: "The total number of hits taken from hybrid turrets"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersMissileAmount:
       name: "Damage Taken from Missiles"
       description: "The total damage taken from missile launchers"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersMissileNumShots:
       name: "Hits Taken from Missiles"
       description: "The total number of hits taken from missile launchers"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersProjectileAmount:
       name: "Damage Taken from Projectiles"
       description: "The total damage taken from projectile turrets"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersProjectileNumShots:
       name: "Hits Taken from Projectiles"
       description: "The total number of hits taken from projectile turrets"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersSmartBombAmount:
       name: "Damage Taken from Smart Bombs"
       description: "The total damage taken from smart bombs"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersSmartBombNumShots:
       name: "Hits Taken from Smart Bombs"
       description: "The total number of hits taken from smart bombs"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersSuperAmount:
       name: "Damage Taken from Doomsdays"
       description: "The total damage taken from doomsday devices"
+      tags: ['damageTaken','pvp']
     combatDamageFromPlayersSuperNumShots:
       name: "Hits Taken from Doomsdays"
       description: "The total number of hits taken from doomsday devices"
+      tags: ['damageDone','pvp']
     combatDamageToPlayersBombAmount:
       name: "Damage Dealt with Bombs"
       description: "The total amount damage dealt to other players using bombs"
+      tags: ['damageDone','pvp']
     combatDamageToPlayersBombNumShots:
       name: "Hits Dealt with Bombs"
       description: "The total number of hits made to other players using bombs"
+      tags: ['damageDone','pvp']
     combatDamageToPlayersEnergyAmount:
       name: "Damage Dealt with Lasers"
       description: "The total amount damage dealt to other players using laser turrets"
+      tags: []
     combatDamageToPlayersEnergyNumShots:
       name: "Number of Laser Shots"
       description: "The total number of shots made with laser turrets"
+      tags: []
     combatDamageToPlayersFighterMissileAmount:
       name: "Damage Dealt with Fighter Bombers"
       description: "The total amount damage dealt to other players using fighter bombers"
+      tags: []
     combatDamageToPlayersFighterMissileNumShots:
       name: "Number of Fighter Bomber Shots"
       description: "The total number of shots made with fighter bombers"
+      tags: []
     combatDamageToPlayersHybridAmount:
       name: "Damage Dealt with Hybrids"
       description: "The total amount damage dealt to other players using hybrid turrets"
+      tags: []
     combatDamageToPlayersHybridNumShots:
       name: "Number of Hybrid Shots"
       description: "The total number of shots made with hybrid turrets"
+      tags: []
     combatDamageToPlayersMissileAmount:
       name: "Damage Dealt with Missiles"
       description: "The total amount damage dealt to other players using missile launchers"
+      tags: []
     combatDamageToPlayersMissileNumShots:
       name: "Number of Missile Shots"
       description: "The total number of shots made with missile launchers"
+      tags: []
     combatDamageToPlayersProjectileAmount:
       name: "Damage Dealt with Projectiles"
       description: "The total amount damage dealt to other players using projectile turrets"
+      tags: []
     combatDamageToPlayersProjectileNumShots:
       name: "Number of Projectile Shots"
       description: "The total number of shots made with projectile turrets"
+      tags: []
     combatDamageToPlayersSmartBombAmount:
       name: "Damage Dealth with Smart Bombs"
       description: "The total amount damage dealt to other players using smart bombs"
+      tags: []
     combatDamageToPlayersSmartBombNumShots:
       name: "Number of Smart Bomb Hits"
       description: "The total number of hits made with smart bombs"
+      tags: []
     combatDamageToPlayersStructureAmount:
       description: "REMOVE: Always 0...#wellshit"
       disabled: true
+      tags: []
     combatDamageToPlayersStructureNumShots:
       description: "REMOVE: Always 0...#wellshit"
       disabled: true
+      tags: []
     combatDamageToPlayersSuperAmount:
       name: "Damage Dealth with Doomsdays"
       description: "The total amount of damage dealt using doomsday devices"
+      tags: []
     combatDamageToPlayersSuperNumShots:
       name: "Number of Doomsday Shots"
       description: "The total number of shots made with doomsday devices"
+      tags: []
     combatDuelRequested:
       names: "Duel Requests"
       description: "The total number of times the character requested a duel against another player"
+      tags: []
     combatNpcFlagSet:
       names: "Aggressed against NPCs"
       description: "The number of times aggression flag was set against NPCs"
+      tags: []
     combatPvpFlagSet:
       name: "Aggressed against Players"
       description: "The number of times aggression flag was set against Players"
+      tags: []
     combatRepairArmorByRemoteAmount:
       name: "Armor Reps Received"
       description: "The total amount received from remote armor repairers"
+      tags: []
     combatRepairArmorRemoteAmount:
       name: "Armor Reps Given"
       description: "The total amount repaired using remote armor repairers"
+      tags: []
     combatRepairArmorSelfAmount:
       name: "Armor Self Repped"
       description: "The total amount repaired using local armor repairers"
+      tags: []
     combatRepairCapacitorByRemoteAmount:
       name: "Energy Transfer Received"
       description: "The total amount of energy received from energy transfers"
+      tags: []
     combatRepairCapacitorRemoteAmount:
       name: "Energy Transfer Given"
       description: "The total amount of energy sent using energy tranfers"
+      tags: []
     combatRepairHullByRemoteAmount:
       name: "Hull Reps Received"
       description: "The total amount received from remote hull repairers"
+      tags: []
     combatRepairHullRemoteAmount:
       name: "Hull Reps Given"
       description: "The total amount repaired using remote hull repairers"
+      tags: []
     combatRepairHullSelfAmount:
       name: "Hull Self Repped"
       description: "The total amount repaired using local hull repairers"
+      tags: []
     combatRepairShieldByRemoteAmount:
       name: "Shield Reps Receieved"
       description: "The total amount received from remote shield transfers"
+      tags: []
     combatRepairShieldRemoteAmount:
       name: "Shield Reps Given"
       description: "The total amount repaired using remote shield transfers"
+      tags: []
     combatRepairShieldSelfAmount:
       name: "Shield Self Repped"
       description: "The total amount repaired using local shield transfers"
+      tags: []
     combatSelfDestructs:
       name: "Self Destructs"
       description: "The number of successful self destructs"
+      tags: []
     combatWarpScramblePC:
       name: "Scrammed Player"
       description: "The number of times warp scrambled other players"
+      tags: []
     combatWarpScrambledbyNPC:
       name: "Scrammed by NPC"
       description: "The number of times warp scrambled by NPCs"
+      tags: []
     combatWarpScrambledbyPC:
       name: "Scrammed by Player"
       description: "The number of times warp scrambled by other players"
+      tags: []
     combatWebifiedbyNPC:
       name: "Webbed Player"
       description: "The number of times webbed other players"
+      tags: []
     combatWebifiedbyPC:
       name: "Webbed by NPCs"
       description: "The number of times webbed by NPCs"
+      tags: []
     combatWebifyingPC:
       name: "Webbed by Player"
       description: "The number of times webbed by other players"
+      tags: []
     genericConeScans:
       name: "Times Dscanned"
       description: "The number of directional scans made"
+      tags: []
     genericRequestScans:
       name: "Probe Scans"
       description: "The number of probe scans made"
+      tags: []
     industryArcheologySuccesses:
       name: "Relic Cans Hacked"
       description: "The number of successful archeology attempts"
+      tags: []
     industryHackingSuccesses:
       name: "Data Cans Hacked"
       description: "The number of successful hacking attempts"
+      tags: []
     industryRamJobsCompletedCopyBlueprint:
       name: "Blueprints Copied"
       description: "The number of copy jobs completed"
+      tags: []
     industryRamJobsCompletedInvention:
       name: "Invention Jobs"
       description: "The number of invention jobs completed"
+      tags: []
     industryRamJobsCompletedManufacture:
       name: "Manufacturing Jobs"
       description: "The total number of manufacturing jobs completed"
+      tags: []
     industryRamJobsCompletedManufactureAsteroidQuantity:
       name: "Mineral Compression Jobs"
       description: "The total units of"
+      tags: []
     industryRamJobsCompletedManufactureChargeQuantity:
       name: "Charges Produced"
       description: "The total units of charges produced"
+      tags: []
     industryRamJobsCompletedManufactureCommodityQuantity:
       name: "Commodities Produced"
       description: "The total units of commodities produced"
+      tags: []
     industryRamJobsCompletedManufactureDeployableQuantity:
       name: "Deployables Produced"
       description: "The total units of deployables produced"
+      tags: []
     industryRamJobsCompletedManufactureDroneQuantity:
       name: "Drones Produced"
       description: "The total units of drones produced"
+      tags: []
     industryRamJobsCompletedManufactureImplantQuantity:
       name: "Implants Produced"
       description: "The total units of implants produced"
+      tags: []
     industryRamJobsCompletedManufactureModuleQuantity:
       name: "Modules Produced"
       description: "The total units of modules produced"
+      tags: []
     industryRamJobsCompletedManufactureShipQuantity:
       name: "Ships Produced"
       description: "The total units of ships produced"
+      tags: []
     industryRamJobsCompletedManufactureStructureQuantity:
       name: "Structures Produced"
       description: "The total units of structures produced"
+      tags: []
     industryRamJobsCompletedManufactureSubsystemQuantity:
       name: "Subsystems Produced"
       description: "The total units of subsystems produced"
+      tags: []
     industryRamJobsCompletedMaterialProductivity:
       name: "ME Jobs"
       description: "The number of material efficiency jobs completed"
+      tags: []
     industryRamJobsCompletedReverseEngineering:
       name: "Reverse Engineering Jobs"
       description: "The number of reverse engineering jobs completed (merged into invention as of ?)"
+      tags: []
     industryRamJobsCompletedTimeProductivity:
       name: "TE Jobs"
       description: "The number of production efficiency jobs completed"
+      tags: []
     inventoryTrashItemQuantity:
       # stack size or number of times used?
       name: "Items Trashed"
       description: "The number of items trashed"
+      tags: []
     marketIskIn:
       name: "ISK Received from Sell Orders"
       description: "The amount of isk from sell-orders"
+      tags: []
     marketIskOut:
       name: "ISK Spent on Buy Orders"
       description: "The amount of isk from buy-orders"
+      tags: []
     marketAcceptContractsCourier:
       name: "Courier Contracts Accepted"
       description: "The number of times accepted a courier contract"
+      tags: []
     marketAcceptContractsItemExchange:
       name: "Item Contracts Accepted"
       description: "The number of times accepted an item exchange contract"
+      tags: []
     marketBuyOrdersPlaced:
       name: "Buy Orders Placed"
       description: "The number of buy orders placed"
+      tags: []
     marketSellOrdersPlaced:
       name: "Sell Orders Placed"
       description: "The number of sell orders placed"
+      tags: []
     marketCancelMarketOrder:
       name: "Orders Cancelled"
       description: "The number of orders cancelled"
+      tags: []
     marketCreateContractsTotal:
       # personal only?  corp contracts as well?
       name: "Contracts Created"
       description: "The number of contracts created"
+      tags: []
     marketDeliverCourierContract:
       name: "Courier Contracts Delivered"
       description: "The number of courier contracts delivered"
+      tags: []
     marketISKGained:
       description: "REMOVE: historical data is bugged, supposed to take only sell orders to other players"
       disabled: true
+      tags: []
     marketISKSpent:
       description: "REMOVE: historical data is bugged, supposed to take only buy orders to other players"
       disabled: true
+      tags: []
     marketModifyMarketOrder:
       name: "Orders Modified"
       description: "The number of modifications made to market orders"
+      tags: []
     miningOreArkonor:
       name: "Arkanor Mined"
       description: "The total amount of Arkonor mined (units)"
+      tags: []
     miningOreBistot:
       name: "Bistot Mined"
       description: "The total amount of Bistot mined (units)"
+      tags: []
     miningOreCrokite:
       name: "Crokite Mined"
       description: "The total amount of Crokite mined (units)"
+      tags: []
     miningOreDarkOchre:
       name: "Dark Ochre Mined"
       description: "The total amount of DarkOchre mined (units)"
+      tags: []
     miningOreGneiss:
       name: "Gneiss Mined"
       description: "The total amount of Gneiss mined (units)"
+      tags: []
     miningOreHarvestableCloud:
       name: "Gas Huffed"
       description: "The total amount of HarvestableCloud mined (units)"
+      tags: []
     miningOreHedbergite:
       name: "Hedbergite Mined"
       description: "The total amount of Hedbergite mined (units)"
+      tags: []
     miningOreHemorphite:
       name: "Hemorphite Mined"
       description: "The total amount of Hemorphite mined (units)"
+      tags: []
     miningOreIce:
       name: "Ice Mined"
       description: "The total amount of Ice mined (units)"
+      tags: []
     miningOreJaspet:
       name: "Jaspet Mined"
       description: "The total amount of Jaspet mined (units)"
+      tags: []
     miningOreKernite:
       name: "Kernite Mined"
       description: "The total amount of Kernite mined (units)"
+      tags: []
     miningOreMercoxit:
       name: "Mercoxit Mined"
       description: "The total amount of Mercoxit mined (units)"
+      tags: []
     miningOreOmber:
       name: "Omber Mined"
       description: "The total amount of Omber mined (units)"
+      tags: []
     miningOrePlagioclase:
       name: "Plagioclase Mined"
       description: "The total amount of Plagioclase mined (units)"
+      tags: []
     miningOrePyroxeres:
       name: "Pyroxeres Mined"
       description: "The total amount of Pyroxeres mined (units)"
+      tags: []
     miningOreScordite:
       name: "Scordite Mined"
       description: "The total amount of Scordite mined (units)"
+      tags: []
     miningOreSpodumain:
       name: "Spodumain Mined"
       description: "The total amount of Spodumain mined (units)"
+      tags: []
     miningOreVeldspar:
       name: "Veldspar Mined"
       description: "The total amount of Veldspar mined (units)"
+      tags: []
     moduleActivationsCloaking:
       name: "Times Cloaked"
       description: "The total number of cloak activations"
+      tags: []
     moduleActivationsCyno:
       name: "Cynos Activated"
       description: "The total number of cyno activations"
+      tags: []
     moduleActivationsEwarDampener:
       name: "Sensor Damps Used"
       description: "The total number of sensor dampener activations"
+      tags: []
     moduleActivationsEwarECM:
       name: "ECM Activations"
       description: "The total number of ECM activations"
+      tags: []
     moduleActivationsEwarTargetPainter:
       name: "Target Painter Activations"
       description: "The total number of target painter activations"
+      tags: []
     moduleActivationsEwarVampire:
       name: "Energy Vampire Activations"
       description: "The total number of energy vampire activations"
+      tags: []
     moduleActivationsFleetAssist:
       name: "Links Activated"
       description: "The total number of gang link module activations"
+      tags: []
     moduleOverload:
       name: "Modules Overloaded"
       description: "The number of times a module was overloaded"
+      tags: []
     socialAddContactBad:
       name: "Added Contact with Bad Standing"
       description: "The number of contacts added with bad standings"
+      tags: []
     socialAddContactGood:
       name: "Added Contact with Good Standing"
       description: "The number of contacts added with good standings"
+      tags: []
     socialAddContactHigh:
       name: "Added Contact with High Standing"
       description: "The number of contacts added with high standings"
+      tags: []
     socialAddContactHorrible:
       name: "Added Contact with Horrible Standing"
       description: "The number of contacts added with horrible standings"
+      tags: []
     socialAddContactNeutral:
       name: "Added Contact with Neutral Standing"
       description: "The number of contacts added with neutral standings"
+      tags: []
     socialAddedAsContactBad:
       name: "Added as Bad Contact by Another Player"
       description: "The number of times added as a contact with bad standings"
+      tags: []
     socialAddedAsContactGood:
       name: "Added as Good Contact by Another Player"
       description: "The number of times added as a contact with good standings"
+      tags: []
     socialAddedAsContactHigh:
       name: "Added as High Contact by Another Player"
       description: "The number of times added as a contact with high standings"
+      tags: []
     socialAddedAsContactHorrible:
       name: "Added as Horrible Contact by Another Player"
       description: "The number of times added as a contact with horrible standings"
+      tags: []
     socialAddedAsContactNeutral:
       name: "Added as Neutral Contact by Another Player"
       description: "The number of times added as a contact with neutral standings"
+      tags: []
     socialChatTotalMessageLength:
       name: "Total Length of All Chat"
       description: "The total length of all chat messages -NSA"
+      tags: []
     socialDirectTrades:
       name: "Direct Trades"
       description: "The number of direct trades made"
+      tags: []
     socialFleetJoins:
       name: "Fleets Joined"
       description: "The number of fleets joined"
+      tags: []
     socialFleetBroadcasts:
       name: "Fleet Broadcasts"
       description: "The number of broadcasts made in fleet"
+      tags: []
     socialMailsReceived:
       name: "Mails Received"
       description: "The number of mails received -NSA"
+      tags: []
     socialMailsSent:
       name: "Mails Sent"
       description: "The number of mails sent -NSA"
+      tags: []
     travelAccelerationGateActivations:
       name: "Acceleration Gates Activated"
       description: "The number of acceleration gates activated"
+      tags: []
     travelAlignTo:
       name: "Times Aligned"
       description: "The number of times ship alignment was made"
+      tags: []
     travelDistanceWarpedHighSec:
       name: "AU Traveled in High Sec"
       description: "The total distance(AU) traveled in warp while in high security space"
+      tags: []
     travelDistanceWarpedLowSec:
       name: "AU Traveled in Low Sec"
       description: "The total distance(AU) traveled in warp while in low security space"
+      tags: []
     travelDistanceWarpedNullSec:
       name: "AU Traveled in Null Sec"
       description: "The total distance(AU) traveled in warp while in null security space"
+      tags: []
     travelDistanceWarpedWormhole:
       name: "AU Traveled in Wormholes"
       description: "The total distance(AU) traveled in warp while in wormhole space"
+      tags: []
     travelWarpsHighSec:
       name: "Initiated Warp in High Sec"
       description: "The total number of warps initiated while in high security space"
+      tags: []
     travelWarpsLowSec:
       name: "Initiated Warp in Low Sec"
       description: "The total number of warps initiated while in low security space"
+      tags: []
     travelWarpsNullSec:
       name: "Initiated Warp in Null Sec"
       description: "The total number of warps initiated while in null security space"
+      tags: []
     travelWarpsWormhole:
       name: "Initiated Warp in Wormholes"
       description: "The total number of warps initiated while in wormhole space"
+      tags: []
     travelWarpsToBookmark:
       name: "Initiated Warp to Bookmark"
       description: "The total number of warps initiated to a bookmark"
+      tags: []
     travelWarpsToCelestial:
       name: "Initiated Warp to Celestial"
       description: "The total number of warps initiated to a celestial"
+      tags: []
     travelWarpsToFleetMember:
       name: "Initiated Warp to Fleet Member"
       description: "The total number of warps initiated to a fleet member"
+      tags: []
     travelWarpsToScanResult:
       name: "Initiated Warp to Scan Result"
       description: "The total number of warps initiated to a scan result"
+      tags: []

     # derived fields
     combatKillsTotal:
       name: 'Total Kills'
+      tags: ['pvp']
     combatDeathsTotal:
       name: 'Total Deaths'
+      tags: ['pvp']
+    iskSpentPercent:
+      name: 'Percent of Income Spent, Overall'
+      tags: ['general']
+      units: 'percent'
+    kdRatio:
+      name: 'Kill/Death Ratio'
+      tags: ['pvp']
+    averageSessionLength:
+      name: 'Average Session Length'
+      tags: ['general']
+      units: 'minutes'

 module.exports = CharacterStats


webapp/jupyter2: more refactoring

webapp/jupyter2: more refactoring

--- src/smc-webapp/jupyter/actions.coffee
+++ src/smc-webapp/jupyter/actions.coffee
@@ -251,7 +251,8 @@
     # Set the input of the given cell in the syncdb, which will also change the store.
     # Might throw a CellWriteProtectedException
     set_cell_input: (id, input, save=true) =>
-        @_checked_set
+        return if @store.check_edit_protection(id, @)
+        @_set
             type  : 'cell'
             id    : id
             input : input
@@ -260,7 +261,7 @@
             save

     set_cell_output: (id, output, save=true) =>
-        @_checked_set
+        @_set
             type   : 'cell'
             id     : id
             output : output,
@@ -271,25 +272,21 @@
         v = @store.get_selected_cell_ids_list()
         for id in v
             cell = cells.get(id)
-            try
-                if cell.get('output')? or cell.get('exec_count')
-                    @_set({type:'cell', id:id, output:null, exec_count:null}, false)
-            catch ex
-                if ex is CellWriteProtectedException
-                    if v.length == 1
-                        @show_edit_protection_error()
-                else
-                    throw ex
+            if not @store.is_cell_editable(id)
+                if v.length == 1
+                    @show_edit_protection_error()
+                continue
+            if cell.get('output')? or cell.get('exec_count')
+                @_set({type:'cell', id:id, output:null, exec_count:null}, false)
         @_sync()

     clear_all_outputs: =>
         @store.get('cells').forEach (cell, id) =>
             if cell.get('output')? or cell.get('exec_count')
-                try
+                if not @store.is_cell_editable(id)
+                    @show_edit_protection_error()
+                else
                     @_set({type:'cell', id:id, output:null, exec_count:null}, false)
-                catch ex
-                    if ex isnt CellWriteProtectedException
-                        throw ex
             return
         @_sync()

@@ -319,9 +316,7 @@
     set_cell_type: (id, cell_type='code') =>
         if cell_type != 'markdown' and cell_type != 'raw' and cell_type != 'code'
             throw Error("cell type (='#{cell_type}') must be 'markdown', 'raw', or 'code'")
-        if not @store.is_cell_editable(id)
-            @show_edit_protection_error()
-            return
+        return if @store.check_edit_protection(id, @)
         obj =
             type      : 'cell'
             id        : id
@@ -347,8 +342,7 @@
         md_edit_ids = @store.get('md_edit_ids')
         if md_edit_ids.contains(id)
             return
-        if not @store.is_cell_editable(id)
-            throw CellWriteProtectedException
+        return if @store.check_edit_protection(id, @)
         @setState(md_edit_ids : md_edit_ids.add(id))

     set_md_cell_not_editing: (id) =>
@@ -358,14 +352,8 @@
         @setState(md_edit_ids : md_edit_ids.delete(id))

     change_cell_to_heading: (id, n=1) =>
-        try
-            @set_md_cell_editing(id)
-        catch ex
-            if ex is CellWriteProtectedException
-                @show_edit_protection_error()
-                return
-            else
-                throw ex
+        return if @store.check_edit_protection(id, @)
+        @set_md_cell_editing(id)
         @set_cell_type(id, 'markdown')
         input = misc.lstrip(@_get_cell_input(id))
         i = 0
@@ -658,15 +646,6 @@
         # ensure that we update locally immediately for our own changes.
         @_syncdb_change(immutable.fromJS([misc.copy_with(obj, ['id', 'type'])]))

-    _checked_set: (obj, save=true) =>
-        try
-            @_set(obj, save)
-        catch ex
-            if ex is CellWriteProtectedException
-                @show_edit_protection_error()
-            else
-                throw ex
-
     # might throw a CellDeleteProtectedException
     _delete: (obj, save=true) =>
         if @_state == 'closed'
@@ -732,13 +711,10 @@
             @move_cursor_before(selected[0])
         not_deletable = 0
         for id in selected
-            try
+            if not @store.is_cell_deletable(id)
+                not_deletable += 1
+            else
                 @_delete({type:'cell', id:id}, false)
-            catch ex
-                if ex is CellDeleteProtectedException
-                    not_deletable += 1
-                else
-                    throw ex
         if sync
             @_sync()
         if not_deletable > 0


VueI18n: force Vue to rerender after locale change. There is a intermittent bug where non en locales sometimes dont render in all components when the page loads

--- vue/src/shared/services/session.coffee
+++ vue/src/shared/services/session.coffee
@@ -37,6 +37,7 @@
         loadedLocales.push(locale)
         i18n.setLocaleMessage(locale, data)
         setI18nLanguage(locale)
+        Vue.forceUpdate()

 export default new class Session
   defaultFormat: ->


activity: render new message modal on top of current pane

--- client/activity/lib/reactivityroutes.coffee
+++ client/activity/lib/reactivityroutes.coffee
@@ -32,7 +32,7 @@
   childRoutes: [
     path: 'New'
     components:
-      content: null
+      content: ChannelThreadPane
       modal: CreatePublicChannelModal
   ,
     path: ':channelName(/:postId)'


Delete nascent backbone file.

Delete nascent backbone file.

--- public/backbone/routers/application_router.coffee
+++ public/backbone/routers/application_router.coffee
@@ -1,14 +0,0 @@
-
-class ApplicationRouter extends Backbone.Router
-  routes:
-    "": "index"
-
-  constructor: ->
-
-  index: ->
-    alert 'index'
-
-$ ->
-  new ApplicationRouter()
-  Backbone.history.start({pushState: true})
-


Add change item to editor context menu

Add change item to editor context menu

--- menus/encoding-selector.cson
+++ menus/encoding-selector.cson
@@ -5,3 +5,9 @@
     'command': 'encoding-selector:show'
   ]
 ]
+
+'context-menu':
+  '.overlayer': [
+    'label': 'Change Encoding'
+    'command': 'encoding-selector:show'
+  ]


pubnub: fix client-side toggling

pubnub: fix client-side toggling

--- client/Main/KD.extend.coffee
+++ client/Main/KD.extend.coffee
@@ -270,12 +270,12 @@
       callback null, owner

   togglePubnub: ->
-    isPubnubEnabled = localStorage.isPubnubEnabled or KD.config.pubnub.enabled
+    isPubnubEnabled = localStorage.isPubnubEnabled is "true" or KD.config.pubnub.enabled
     localStorage.isPubnubEnabled = !isPubnubEnabled
     location.reload()

   isPubnubEnabled: ->
-    localStorage.isPubnubEnabled or KD.config.pubnub.enabled
+    localStorage.isPubnubEnabled is "true" or KD.config.pubnub.enabled

 Object.defineProperty KD, "defaultSlug",


code clean

code clean

--- client/app/views/main.coffee
+++ client/app/views/main.coffee
@@ -124,7 +124,8 @@
     changeFavicon: (url) ->
         @favicon?.remove()
         @favicon2?.remove()
-        @favicon = $ '<link rel="icon" type="image/x-icon" href="'+url+'" />"'
+        newfav = '<link rel="icon" type="image/x-icon" href="' + url + '" />"'
+        @favicon = $ newfav
         @favicon2 = @favicon.clone().attr 'rel', 'shortcut icon'
         $('head').append @favicon, @favicon2


Fix bug about iframe hash change when intent

--- client/app/views/main.coffee
+++ client/app/views/main.coffee
@@ -267,11 +267,9 @@
             # if the app was already open, we want to change its hash
             # only if there is a hash in the home given url.
             else if hash
-                contentWindow = frame.prop('contentWindow')
-
                 # Same origin policy may prevent to access location hash
                 try
-                    currentHash = contentWindow.location.hash.substring 1
+                    frame.prop('contentWindow').location.hash = hash
                 catch err
                     console.err err
                 onLoad()
@@ -356,4 +354,3 @@
     displayToken: (token, slug) ->
         iframeWin = document.getElementById("#{slug}-frame").contentWindow
         iframeWin.postMessage token: token, appName:slug, '*'
-


Use a better method to keep scroll at bottom

--- client/views/app/room.coffee
+++ client/views/app/room.coffee
@@ -481,6 +481,10 @@
 			if error
 				toastr.error error.reason

+	'load img': (e, template) ->
+		template.sendToBottomIfNecessary?()
+
+
 Template.room.onCreated ->
 	# this.scrollOnBottom = true
 	# this.typing = new msgTyping this.data._id
@@ -508,15 +512,70 @@
 	# ScrollListener.init()

 	wrapper = this.find('.wrapper')
+	wrapperUl = this.find('.wrapper > ul')
 	newMessage = this.find(".new-message")

 	template = this

 	wrapperOffset = $('.messages-box > .wrapper').offset()

-	onscroll = _.throttle ->
-		template.atBottom = wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight
-	, 200
+	template.isAtBottom = ->
+		if wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight
+			newMessage.className = "new-message not"
+			return true
+		return false
+
+	template.sendToBottom = ->
+		wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight
+		newMessage.className = "new-message not"
+
+	template.checkIfScrollIsAtBottom = ->
+		template.atBottom = template.isAtBottom()
+		readMessage.enable()
+		readMessage.read()
+
+	template.sendToBottomIfNecessary = ->
+		if template.atBottom is true and template.isAtBottom() isnt true
+			template.sendToBottom()
+
+	template.sendToBottomIfNecessaryDebounced = _.debounce template.sendToBottomIfNecessary, 10
+
+	template.sendToBottomIfNecessary()
+
+	if not window.MutationObserver?
+		wrapperUl.addEventListener 'DOMSubtreeModified', ->
+			template.sendToBottomIfNecessaryDebounced()
+	else
+		observer = new MutationObserver (mutations) ->
+			mutations.forEach (mutation) ->
+				template.sendToBottomIfNecessaryDebounced()
+
+		observer.observe wrapperUl,
+			childList: true
+		# observer.disconnect()
+
+	wrapper.addEventListener 'mousewheel', ->
+		template.atBottom = false
+		Meteor.defer ->
+			template.checkIfScrollIsAtBottom()
+
+	wrapper.addEventListener 'wheel', ->
+		template.atBottom = false
+		Meteor.defer ->
+			template.checkIfScrollIsAtBottom()
+
+	wrapper.addEventListener 'touchstart', ->
+		template.atBottom = false
+
+	wrapper.addEventListener 'touchend', ->
+		Meteor.defer ->
+			template.checkIfScrollIsAtBottom()
+		Meteor.setTimeout ->
+			template.checkIfScrollIsAtBottom()
+		, 1000
+		Meteor.setTimeout ->
+			template.checkIfScrollIsAtBottom()
+		, 2000

 	updateUnreadCount = _.throttle ->
 		firstMessageOnScreen = document.elementFromPoint(wrapperOffset.left+1, wrapperOffset.top+50)
@@ -528,37 +587,6 @@
 			else
 				template.unreadCount.set 0
 	, 300
-
-	Meteor.setInterval ->
-		if template.atBottom
-			wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight
-			newMessage.className = "new-message not"
-	, 100
-
-	wrapper.addEventListener 'touchstart', ->
-		template.atBottom = false
-
-	wrapper.addEventListener 'touchend', ->
-		onscroll()
-		readMessage.enable()
-		readMessage.read()
-
-	wrapper.addEventListener 'scroll', ->
-		template.atBottom = false
-		onscroll()
-		updateUnreadCount()
-
-	wrapper.addEventListener 'mousewheel', ->
-		template.atBottom = false
-		onscroll()
-		readMessage.enable()
-		readMessage.read()
-
-	wrapper.addEventListener 'wheel', ->
-		template.atBottom = false
-		onscroll()
-		readMessage.enable()
-		readMessage.read()

 	# salva a data da renderização para exibir alertas de novas mensagens
 	$.data(this.firstNode, 'renderedAt', new Date)


Server: API/Createuser: use async.waterfall in handleUsername helper

--- servers/lib/server/handlers/api/createuser.coffee
+++ servers/lib/server/handlers/api/createuser.coffee
@@ -104,34 +104,42 @@

   queue = []

-  if username
-    queue.push (next) ->
-      validateUsername username, (err) ->
-        # return username if it is valid
-        return callback null, username  unless err
-        # return if there is no suggestedUsername
-        return callback err  unless suggestedUsername
-        next()
+  generateUsername = (next, results) ->
+    _username = "#{suggestedUsername}#{hat(32)}"
+    validateUsername _username, (err) ->
+      return next err  if err
+      next null, _username

-  if suggestedUsername
-    queue.push (next) ->
-      # if suggestedUsername length is not valid, return error without trying
-      unless isSuggestedUsernameLengthValid suggestedUsername
-        return callback apiErrors.outOfRangeSuggestedUsername
-      next()
+  queue.push (next) ->
+    # go next step and try suggestedUsername if no username is given
+    return next null, null  unless username

-    # try usernames with different suffixes 10 times
-    for i in [0..10]
-      queue.push (next) ->
-        _username = "#{suggestedUsername}#{hat(32)}"
-        validateUsername _username, (err) ->
-          return callback null, _username  unless err
-          next()
+    validateUsername username, (err) ->
+      if err
+        # try with suggestedUsername if one is given
+        return next null, null  if suggestedUsername
+        return next err

-    # if username is still invalid, let it go
-    queue.push -> callback apiErrors.usernameAlreadyExists
+      # no err, pass username to next function
+      next null, username

-  async.series queue
+  queue.push (username_, next) ->
+    # skip if username is returned from previous function
+    return next null, username_  if username_
+
+    # if suggestedUsername length is not valid, return error without trying
+    unless isSuggestedUsernameLengthValid suggestedUsername
+      return next apiErrors.outOfRangeSuggestedUsername
+
+    # try 10 times to generate a valid username
+    # will stop trying after first successful attempt
+    async.retry 10, generateUsername, (err, generatedUsername) ->
+      return next err  if err
+      next null, generatedUsername
+
+  async.waterfall queue, (err, username_) ->
+    return callback err  if err
+    return callback null, username_

 validateUsername = (username, callback) ->


fixed problem where 2+ word models wouldn't work with sortable jquery

--- app/assets/javascripts/active_admin/components/jquery.reorder.coffee
+++ app/assets/javascripts/active_admin/components/jquery.reorder.coffee
@@ -5,7 +5,7 @@
     $table.find("tbody").sortable
       update: (e, ui) ->
         $tr = $(@).find("tr")
-        tr_ids = $tr.map( -> @.id.split("_")[1] ).toArray().join(",")
+        tr_ids = $tr.map( -> @.id.split("_").pop() ).toArray().join(",")
         route = "#{resource}/sort"
         console.log(route)
         $.post "#{resource}/sort", sort: tr_ids, ->
@@ -19,4 +19,4 @@
       helper: (e, ui) ->
         ui.children().each ->
           $(@).width($(@).width())
-        ui+        ui


Update validations_router.js.coffee

Update validations_router.js.coffee

Changed MaxZoom to 18

--- app/assets/javascripts/backbone/routers/validations_router.js.coffee
+++ app/assets/javascripts/backbone/routers/validations_router.js.coffee
@@ -173,7 +173,7 @@
     center = args.center || [24.5, 54]
     zoom = args.zoom || 9

-    baseMap = L.tileLayer('http://tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png', {maxZoom: 18})
+    baseMap = L.tileLayer('http://tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{z}/{x}/{y}.png', {maxZoom: 19})
     baseSatellite =  new L.BingLayer("ApZALeudlU-OTm7Me2qekFHrstBXNdv3hft6qy3ZeTQWD6a460-QqCQyYnDigINc", {type: "Aerial", maxZoom: 19})

     map = L.map map_id,


activity: pass props to super call

activity: pass props to super call

--- client/activity/lib/components/createpublicchannelmodal/index.coffee
+++ client/activity/lib/components/createpublicchannelmodal/index.coffee
@@ -18,11 +18,13 @@

 module.exports = class CreatePublicChannelModal extends React.Component

+
   @include [DropboxInputMixin]

+
   constructor: (props) ->

-    super
+    super props

     @state =
       name                : ''


Set notification events in backbone event hash

--- components/logged_in_navigation/components/notifications/view.coffee
+++ components/logged_in_navigation/components/notifications/view.coffee
@@ -6,6 +6,8 @@
 module.exports = class NotificationsView extends Backbone.View
   events:
     'click a': -> delay (=> @render()), 250
+    'mouseleave': 'markAsRead'
+    'click': 'markAsRead'

   initialize: ({ @state }) ->
     @listenTo @collection, 'sync', @render
@@ -20,9 +22,9 @@
             @collection.clear()
             @state.set('is_fetching', false)

-      .on 'mouseleave click', (e) =>
-        @collection.markRead()
-        @state.set('unread_count', 0)
+  markAsRead: ->
+    @collection.markRead()
+    @state.set('unread_count', 0)

   render: ->
     @$el.html template


EmailerWorker: fix linter issues

EmailerWorker: fix linter issues

--- workers/emailer/lib/emailer/emailerworker.coffee
+++ workers/emailer/lib/emailer/emailerworker.coffee
@@ -4,11 +4,13 @@

 Tracker  = require '../../../social/lib/social/models/tracker.coffee'

-sendmail = (require 'sendmail') logger : {  # disable verbosed loggers
-  debug  : ->
-  info   : ->
-  warn   : ->
-  error  : ->
+sendmail = (require 'sendmail') {
+  logger   : {  # disable verbosed loggers
+    debug  : ->
+    info   : ->
+    warn   : ->
+    error  : ->
+  }
 }

 module.exports = class EmailerWorker


code cleanup: remove unused class

code cleanup: remove unused class

--- workers/social/lib/social/models/kiteapps.coffee
+++ workers/social/lib/social/models/kiteapps.coffee
@@ -1,76 +0,0 @@
-jraphical = require 'jraphical'
-
-module.exports = class JKiteCall extends jraphical.Module
-
-  {Relationship} = jraphical
-
-  {secure} = require 'bongo'
-
-  @share()
-
-  @set
-    softDelete : yes
-    permissions: [
-      'read kiteapps'
-      'create kiteapps'
-      'edit kiteapps'
-      'delete kiteapps'
-      'delete own kiteapps'
-    ]
-    sharedMethods   :
-      instance      : [
-          'delete'
-        ]
-      static        : [
-          'create', 'get', 'inc'
-        ]
-    schema          :
-      username      :
-        type        : String
-        required    : yes
-      methodName    :
-        type        : String
-        required    : yes
-      kiteName      :
-        type        : String
-        required    : yes
-      count         :
-        type        : Number
-        required    : no
-
-  @create = (data, callback)->
-    data.count = 1
-    kiteApp = new JKiteCall data
-    kiteApp.save (err)->
-      if err
-        callback err
-      else
-        callback null, kiteApp
-
-  @get = secure (data, callback)->
-
-    @one {
-     username : data.username
-     kiteName : data.kiteName
-     methodName : data.methodName
-    }, (err, appData)=>
-      if err
-        callback err
-      else
-        callback null, appData
-
-  @inc = (data, callback)->
-
-    @get data , (err, appData)=>
-      if err
-        callback err
-      else
-        if appData instanceof JKiteCall
-
-          appData.update {$inc: 'count': 1} , (err) =>
-            callback null, appData
-        else
-          @create data, callback
-
-  delete: secure ({connection:{delegate}}, callback)->
-    @remove callback


Set both options and defaults for content

--- vendor/assets/javascripts/expressionui.popover.js.coffee
+++ vendor/assets/javascripts/expressionui.popover.js.coffee
@@ -32,7 +32,7 @@
       if typeof options.content is "string"
         $('.popover').remove()
         $('body').append '
' + options.content + '
'
-        methods.defaults.content = $('.popover')
+        options.content = methods.defaults.content = $('.popover')
       options = $.extend({}, methods.defaults, options)
       options.tooltipHeight = 0
       options.tooltipWidth = 0


Fixed case list view spinner never disappearing

--- static/coffee/services.coffee
+++ static/coffee/services.coffee
@@ -272,7 +272,7 @@
       $http.get('/case/search/?' + $.param(params))
       .success((data) =>
         @_processCases(data.results)
-        callback(data.results, data.total, data.has_more)
+        callback(data.results, data.has_more)
       ).error(DEFAULT_ERR_HANDLER)

     #----------------------------------------------------------------------------


Added failing test for issue 2

Added failing test for issue 2

--- test/index.coffee
+++ test/index.coffee
@@ -5,6 +5,16 @@

 Lock = require('../index').Lock
 LockCollection = require('../index').LockCollection
+
+# Used for simulating dead write requests
+setWriteReq = (lock, cb) ->
+  lock.collection.findAndModify(
+    { files_id: lock.fileId }
+    []
+    { $set: { write_req: true }}
+    {w: lock.lockCollection.writeConcern}
+    cb
+  )

 describe 'gridfs-locks', () ->

@@ -558,7 +568,7 @@
                 done()
             lock1.releaseLock().on 'released', (ld) ->
               assert ld?
-              order += '1');
+              order += '1')

     it "should give priority to a write request waiting on a write lock over a subsequent read request", (done) ->
       expectedOrder = "112233"
@@ -583,7 +593,7 @@
                 done()
             lock1.releaseLock().on 'released', (ld) ->
               assert ld?
-              order += '1');
+              order += '1')

     it "should allow a read request to proceed when a prior write request times out", (done) ->
       expectedOrder = "1331"
@@ -606,7 +616,7 @@
                   assert ld?
                   order += '1'
                   assert.equal order, expectedOrder
-                  done());
+                  done())

   describe 'lock expiration', () ->

@@ -731,7 +741,7 @@
                 assert ld?
                 order += '3'
                 assert.equal order, expectedOrder
-                done());
+                done())

     it "should give priority to a write request waiting on a dead write lock over a subsequent read request", (done) ->
       expectedOrder = "12233"
@@ -753,9 +763,9 @@
                 assert ld?
                 order += '3'
                 assert.equal order, expectedOrder
-                done());
-
-    it "should allow a read request to proceed when a prior write request dies without releasing write_req", (done) ->
+                done())
+
+    it "should allow a read request to proceed when a prior write request times-out", (done) ->
       expectedOrder = "1331"
       order = ''
       lock2.timeOut = 100
@@ -778,13 +788,37 @@
                   assert ld?
                   order += '1'
                   assert.equal order, expectedOrder
-                  done());
-
-    it "should allow a read request to proceed when a prior write request dies waiting for a dead write lock without releasing write_req", (done) ->
+                  done())
+
+    it "should allow a read request to proceed when a prior write request dies without releasing write_req", (done) ->
+      expectedOrder = "1331"
+      order = ''
+      lock2.timeOut = 100
+      lock1.lockExpiration = 1000
+      lock3.lockExpiration = 1000
+      lock1.obtainReadLock().on 'locked', (ld) ->
+        assert ld?
+        order += '1'
+        # set the write_req flag without creating a lock, to simulate a dead write request
+        setWriteReq lock2, (err, doc) ->
+          assert not err
+          lock3.obtainReadLock().on 'locked', (ld) ->
+            assert ld?
+            order += '3'
+            lock3.releaseLock().on 'released', (ld) ->
+              assert ld?
+              order += '3'
+              lock1.releaseLock().on 'released', (ld) ->
+                assert ld?
+                order += '1'
+                assert.equal order, expectedOrder
+                done()
+
+    it "should allow a read request to proceed when a prior write request times-out waiting for a write lock", (done) ->
       expectedOrder = "133"
       order = ''
       lock2.timeOut = 100
-      lock3.lockExpiration = 1000
+      lock1.lockExpiration = 1000
       lock1.obtainWriteLock().on 'locked', (ld) ->
         assert ld?
         order += '1'
@@ -799,7 +833,27 @@
                 assert ld?
                 order += '3'
                 assert.equal order, expectedOrder
-                done());
+                done())
+
+    it "should allow a read request to proceed when a prior write request dies waiting for a write lock without releasing write_req", (done) ->
+      expectedOrder = "133"
+      order = ''
+      lock2.timeOut = 100
+      lock1.lockExpiration = 1000
+      lock1.obtainWriteLock().on 'locked', (ld) ->
+        assert ld?
+        order += '1'
+        # set the write_req flag without creating a lock, to simulate a dead write request
+        setWriteReq lock2, (err, doc) ->
+          assert not err
+          lock3.obtainReadLock().on 'locked', (ld) ->
+            assert ld?
+            order += '3'
+            lock3.releaseLock().on 'released', (ld) ->
+              assert ld?
+              order += '3'
+              assert.equal order, expectedOrder
+              done()

     it "of a new read lock shouldn't be affected by when the previous write lock was due to expire", (done) ->
       lock1.lockExpiration = 0  # Never expires


JGroup:tests: setPlan tests

JGroup:tests: setPlan tests

--- workers/social/lib/social/models/group/index.test.coffee
+++ workers/social/lib/social/models/group/index.test.coffee
@@ -468,6 +468,82 @@
         async.series queue

+  describe 'setPlan()', ->
+
+    describe 'when permissions are not valid', ->
+
+      it 'should fail if user is not an admin', (done) ->
+
+        options = { createGroup : yes }
+        withConvertedUser options, ({ client, group }) ->
+          expectAccessDenied group, 'setPlan', done
+
+
+      it 'should fail if user an admin in a group but not in koding', (done) ->
+
+        options = { createGroup: yes, role: 'admin' }
+
+        withConvertedUser options, ({ client, group }) ->
+          group.setPlan client, plan: 'default', (err) ->
+            expect(err).to.exist
+            expect(err.message).to.be.equal 'Access denied'
+
+            done()
+
+
+    describe 'when permissions are ok', ->
+
+      it 'should fail if user wants to set plan for koding group', (done) ->
+
+        options = { role: 'admin' }
+
+        withConvertedUser options, ({ client, group }) ->
+          group.setPlan client, plan: 'default', (err) ->
+            expect(err).to.exist
+            expect(err.message).to.be.equal 'Setting a plan on koding is not allowed'
+
+            done()
+
+      it 'should fail if plan is not supported', (done) ->
+
+        withConvertedUser { createGroup: 'yes' }, ({ group }) ->
+
+          withConvertedUser { role: 'admin' }, ({ client }) ->
+            group.setPlan client, plan: generateRandomString(), (err) ->
+              expect(err).to.exist
+
+              done()
+
+      testGroup = null
+
+      it 'should allow to change if provided plan is valid', (done) ->
+
+        withConvertedUser { createGroup: 'yes' }, ({ group }) ->
+
+          testGroup = group
+
+          withConvertedUser { role: 'admin' }, (data) ->
+            _client = data.client
+
+            group.setPlan _client, plan: 'default', (err) ->
+              expect(err).to.not.exist
+              expect(group.getAt 'config.plan').to.be.equal 'default'
+
+              done()
+
+      it 'should reset plan if "noplan" is provided as plan', (done) ->
+
+        withConvertedUser { role: 'admin' }, ({ client }) ->
+
+          expect(testGroup.getAt 'config.plan').to.be.equal 'default'
+
+          testGroup.setPlan client, plan: 'noplan', (err) ->
+            expect(err).to.not.exist
+            expect(testGroup.getAt 'config.plan').to.not.exist
+
+            done()
+
+
 beforeTests()

 runTests()


group/validators: fix misdirected relationship just for JGroup

--- workers/social/lib/social/models/group/validators.coffee
+++ workers/social/lib/social/models/group/validators.coffee
@@ -55,9 +55,24 @@

       else
         ownerSelector =
-          sourceId    : delegateId
-          targetId    : @getId()
-          as          : 'owner'
+          $or : [
+            {
+              sourceId    : delegateId
+              targetId    : @getId()
+              as          : 'owner'
+            }
+            # cc: ~GG
+            # there is a misdirected relationship between JGroup and JAccount
+            # which prevents previous logic to work for ownership validation
+            # this additional query will cover that one for JGroup only
+            # this must be removed once relationship is updated ~ Hakan
+            {
+              sourceName  : 'JGroup'
+              sourceId    : @getId()
+              targetId    : delegateId
+              as          : 'owner'
+            }
+          ]

         Relationship.count ownerSelector, createExistenceCallback callback


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -507,7 +507,7 @@
     magenta.lightness()
     # => 0.9

-    # modifies magenta in place to be lighter
+    # modifies magenta in place
     magenta.lightness(0.75)

     # to see what it looks like


move logo on mouse move

move logo on mouse move

--- assets/src/site.coffee
+++ assets/src/site.coffee
@@ -51,7 +51,24 @@
   reader.readAsDataURL file

+wWidth = wHeight = 1
+
+setDimensions = ->
+  wWidth  = window.innerWidth
+  wHeight = window.innerHeight
+
+
 document.addEventListener 'DOMContentLoaded', ->
+  setDimensions()
+  logo = document.getElementById 'logo-text'
+
+  addEventListener 'mousemove', ({x, y}) ->
+    logo.style.transform =
+      "translate3d(#{ (x - (wWidth / 2)) / -100 }px, #{ (y - (wHeight / 2)) / -100 }px, 0)"
+
+
+  addEventListener 'resize', setDimensions
+

   document.getElementById('bookmarklet-btn').addEventListener 'click', (e) ->
     e.preventDefault()


adding more destructive methods

adding more destructive methods

--- test/point.coffee
+++ test/point.coffee
@@ -15,6 +15,11 @@

     equals result.x, p1.x + p2.x
     equals result.y, p1.y + p2.y
+
+    equals p1.x, 5
+    equals p1.y, 6
+    equals p2.x, 7
+    equals p2.y, 5

   test "#add with two arguments", ->
     point = Point(3, 7)
@@ -34,6 +39,19 @@
     equals result.x, point.x + x
     equals result.y, point.y + y

+  test "#add$", ->
+    p = Point(0, 0)
+
+    p.add$(Point(3, 5))
+
+    equals p.x, 3
+    equals p.y, 5
+
+    p.add$(2, 1)
+
+    equals p.x, 5
+    equals p.y, 6
+
   test "#subtract", ->
     p1 = Point(5, 6)
     p2 = Point(7, 5)
@@ -42,6 +60,14 @@

     equals result.x, p1.x - p2.x
     equals result.y, p1.y - p2.y
+
+  test "#subtract$", ->
+    p = Point(8, 6)
+
+    p.subtract$(3, 4)
+
+    equals p.x, 5
+    equals p.y, 2

   test "#norm", ->
     p = Point(2, 0)


Fix export

Fix export

--- SingularityUI/app/controllers/DeployDetail.coffee
+++ SingularityUI/app/controllers/DeployDetail.coffee
@@ -12,9 +12,10 @@
   templates:
     header:             require '../templates/deployDetail/deployHeader'
     info:               require '../templates/deployDetail/deployInfo'
-    tasks:              require '../templates/deployDetail/deployTasks'
+    #tasks:              require '../templates/deployDetail/deployTasks'

     initialize: ({@requestId, @deployId}) ->
+      console.log('hi1')
       #
       # Data stuff
       #
@@ -22,10 +23,10 @@
         deployId: @deployId
         requestId: @requestId

-      @collections.deployTasks = new Tasks [],
-        requestId: @requestId
+      #@collections.deployTasks = new Tasks [],
+      #  requestId: @requestId

-      @collections.deployTasks = @collections.deployTasks.where({deployId: @deployId})
+      #@collections.deployTasks = @collections.deployTasks.where({deployId: @deployId})

       #
       # Subviews
@@ -36,9 +37,9 @@
       @subviews.info = new SimpleSubview
         model:      @models.deploy
         template:   @templates.info
-      @subviews.tasks = new SimpleSubview
-        model:      @collections.deployTasks
-        template:   @templates.tasks
+      #@subviews.tasks = new SimpleSubview
+      #  collection:      @collections.deployTasks
+      #  template:   @templates.tasks

       #
@@ -55,4 +56,4 @@
         requestFetch = @models.deploy.fetch()

-        module.exports = DeployDetailController
+module.exports = DeployDetailController


reverted to just leaving one whitespace at end of each line.  I could just strip it on eval.  It works well.

--- salvus/page/worksheet1.coffee
+++ salvus/page/worksheet1.coffee
@@ -52,27 +52,50 @@
                     e = $(document.activeElement)
                     if e.hasClass("salvus-cell-input")
                         console.log("doing it")
-                        #range = rangy.createRange()
-                        #range.selectNode(e[0])
+
                         sel = rangy.getSelection()
                         range = sel.getRangeAt(0)
-                        newNode = $("

")[0]   # need to figure out how to get rid of this space (?)
+
+
+                        #range.pasteHtml("
")
+
+
+                        #return false
+
+
+                        newNode = $("
 ")[0]   # need to figure out how to get rid of this space (?)
                         range.insertNode(newNode)

+
+                        #r2 = rangy.createRange()
+                        #r2.selectNodeContents(newNode)
+                        #r2.moveStart("character", 1)
+                        #sel.setSingleRange(r2)
+                        #sel.deleteFromDocument(r2)
+                        #
+                        sel.removeAllRanges()
                         r2 = rangy.createRange()
                         r2.setStart(newNode,1)
                         r2.setEnd(newNode,1)
                         if sel.rangeCount > 0
                             sel.removeAllRanges()
                         sel.addRange(r2)
-
-                        sel.removeAllRanges()
-
-                        r2 = rangy.createRange()
-                        r2.selectNodeContents(newNode)
-                        r2.moveStart("character", 1)
-                        sel.setSingleRange(r2)
-                        sel.deleteFromDocument(r2)
+                        return false
+
+                        # r2 = rangy.createRange()
+                        # r2.selectNodeContents(newNode)
+                        # r2.moveStart("character", 1)
+                        # sel.setSingleRange(r2)
+                        # sel.deleteFromDocument(r2)
+
+                        # sel.removeAllRanges()
+                        # r2 = rangy.createRange()
+                        # r2.setStart(newNode,1)
+                        # r2.setEnd(newNode,1)
+                        # if sel.rangeCount > 0
+                        #     sel.removeAllRanges()
+                        # sel.addRange(r2)
+
                         return false

             when 40 # down arrow
@@ -173,7 +196,7 @@
         #             #if plain_text.match(/\S/)
         #                 #Rainbow.color(plain_text, opts.language, ((highlighted) -> opts.input.html(highlighted)))
         #             opts.cb?(false, plain_text)
-
+

         console.log(opts.input.html())
         console.log(rangy.innerText(opts.input[0]))


Only update view when path changes

Only update view when path changes

--- src/packages/archive-view/lib/archive-view.coffee
+++ src/packages/archive-view/lib/archive-view.coffee
@@ -17,9 +17,11 @@

     @setModel(editSession)

-  setPath: (@path) ->
-    return unless @path?
+  setPath: (path) ->
+    return unless path?
+    return if @path is path

+    @path = path
     @tree.hide()
     @loadingMessage.show()
     archive.list @path, tree: true, (error, entries) =>


Use a relative require path

Use a relative require path

--- src/packages/editor-stats/lib/editor-stats.coffee
+++ src/packages/editor-stats/lib/editor-stats.coffee
@@ -14,6 +14,6 @@

   createView: ->
     unless @editorStatsView
-      EditorStatsView  = require 'editor-stats/lib/editor-stats-view'
+      EditorStatsView  = require './editor-stats-view'
       @editorStatsView = new EditorStatsView()
     @editorStatsView


Remove comment

Remove comment

--- src/scripts/helpers/backbone/views/popover.coffee
+++ src/scripts/helpers/backbone/views/popover.coffee
@@ -16,7 +16,6 @@
         throw new Error('\'popover.owner\' not defined on object inheriting PopoverView nor passed to constructor')

       @_popover = popoverHandler.createPopover(@_popover)
-      console.log @

     render: () -> return @


victory commit by Bryan, grandcentral works and no errors.

--- server/GrandCentral.coffee
+++ server/GrandCentral.coffee
@@ -1,28 +1,38 @@
-GrandCentral =
-  default_action: ->
-    console.log('GrandCentral works!')
-  users: {
-    insert: (args...) -> GrandCentral.default_action()
-    update: (args...) -> GrandCentral.default_action()
-    remove: (args...) -> GrandCentral.default_action()
-  }
-  posts: {
-    insert: (args...) -> GrandCentral.default_action()
-    update: (args...) -> GrandCentral.default_action()
-    remove: (args...) -> GrandCentral.default_action()
-  }
-  instances: {
-    insert: (args...) -> GrandCentral.default_action()
-    update: (args...) -> GrandCentral.default_action()
-    remove: (args...) -> GrandCentral.default_action()
-  }
+class GrandCentral
+  constructor: (@collection, @method) ->
+    Meteor.default_server.method_handlers["/#{@collection}/#{@method}"] = @dispatch
+  dispatch: (args...) =>
+    {
+      users: {
+        insert: (args...) =>
+          Users.insert(args...)
+        update: (args...) =>
+          Users.update(args...)
+        remove: (args...) =>
+          Users.remove(args...)
+      }
+      posts: {
+        insert: (args...) =>
+          Posts.insert(args...)
+        update: (args...) ->
+          Posts.update(args...)
+        remove: (args...) =>
+          Posts.remove(args...)
+      }
+      instances: {
+        insert: (args...) =>
+          Instances.insert(args...)
+        update: (args...) =>
+          Instances.update(args...)
+        remove: (args...) =>
+          Instances.remove(args...)
+      }
+    }[@collection][@method](args...)
+    console.log "GrandCentral is operational!!"

 Meteor.startup( ->
   for collection in ['users','posts','instances']
     for method in ['insert','update','remove']
-      orig = Meteor.default_server.method_handlers["/#{collection}/#{method}"]
-      Meteor.default_server.method_handlers["/#{collection}/#{method}"] = (args...) ->
-        GrandCentral[collection][method](args...)
-        orig(args...)
+      gc = new GrandCentral(collection, method)
 )


Fixed setOutcome

Fixed setOutcome

--- server/api/setupApi.coffee
+++ server/api/setupApi.coffee
@@ -244,7 +244,8 @@
         if not req.user?.admin
             res.status(403).send('No permissions')
             return
-        setOutcome(req, res)
+        await setOutcome(req, res)
+        res.send('OK')

     app.post '/api/setQuality/:submitId/:quality', ensureLoggedIn, wrap (req, res) ->
         if not req.user?.admin


alter csv response convenience function to execute query params

--- server/lib/aestivus.coffee
+++ server/lib/aestivus.coffee
@@ -278,7 +278,7 @@
     else if @_authAccepted endpointContext, endpoint
       if @_roleAccepted endpointContext, endpoint
         if endpoint.csv is true
-          data = endpoint.action endpointContext
+          data = endpoint.action.call endpointContext
           API.convert.json2csv2response endpointContext, data, endpointContext.request.url.split('/').pop().replace('.csv','') + '_' + moment(Date.now(), "x").format("YYYY_MM_DD_HHmm_ss") + '.csv'
         else
           endpoint.action.call endpointContext


more moves towards bootstrap.

more moves towards bootstrap.

Things are a huge mess right now

--- source/javascripts/src/view_xlformbuilder.coffee
+++ source/javascripts/src/view_xlformbuilder.coffee
@@ -46,7 +46,7 @@

 class XlfRowView extends Backbone.View
   tagName: "li"
-  className: "cl-fixed-row"
+  className: "page-header xlf-row-view"
   events:
    "click .create-new-list": "createListForRow"
    "click .edit-list": "editListForRow"
@@ -65,13 +65,22 @@
       @$el.addClass("xlf-selected")
   render: ->
     @$el.html """
-      ×
-
-<button class="insert-row">+</button>-

-    """
-    @$el.data("row-model-id", @model.cid)
+

+

+

+

+

+

+
-<button class="insert-row">+</button>-

+          <button type="button" class="close" aria-hidden="true">×</button>
+

+

+    """
+    @rowContent = @$(".row-content")

     for [key, val] in @model.attributesArray()
-      v = new XlfDetailView(model: val, rowView: @).renderInRowView(@)
+      new XlfDetailView(model: val, rowView: @).renderInRowView(@)
+
     @
   editListForRow: (evt)->
     @_ensureNoListViewsOpen()
@@ -109,7 +118,7 @@
     @$el.append $lvel

 class @SurveyApp extends Backbone.View
-  className: "formbuilder-wrap"
+  className: "formbuilder-wrap container"
   events:
     "click .delete-row": "clickRemoveRow"
     "click .insert-row": "clickInsertRow"
@@ -135,16 +144,24 @@
     @survey.set("formName", @survey.settings.get("form_title"), silent: true)

     @$el.html """
-

+

-          #{@survey.get("displayTitle")}
-          ##{@survey.get("formName")}
+
+            #{@survey.get("displayTitle")}
+
+          [#{@survey.settings.get("form_title")}]

-
#{@survey.get("displayDescription")}

+

+          #{@survey.get("displayDescription")}
+

-
Loading...

+

+            This survey is currently empty.

+            You can add questions, notes, prompts, or other fields by clicking
+            + Add question below.
+

           <button id="add-question" class="attached"> Add question</button>
@@ -155,14 +172,19 @@
     """
     @formEditorEl = @$(".-form-editor")

-    addOpts      = @$("#additional-options")
-
-    # watch for changes on the title and description
-    # $name = @$(".form-name")
-    # $displayTitle = @$(".display-title")
-    # $descrip = @$(".display-description")
-
-    # _formName = @survey.settings.get("form_title")
+
+    # .form-name maps to settings.form_title
+    @$(".form-name").editInPlace
+      callback: (u, ent)=>
+        val = if ent then XLF.sluggify(ent) else ""
+        @survey.settings.set("form_title", val)
+        if val then val else "..."
+
+    # .display-title maps to first line of settings.description
+    @$(".display-title").editInPlace
+      callback: (u, ent)=>
+        @survey.set("displayTitle", ent)
+        if ent then ent else "..."

     # $displayTitle.text(_displayTitle)
     # $descrip.text(_descrip.join("\n"))
@@ -198,19 +220,13 @@

     @survey.rows.trigger("reset")

+    addOpts = @$("#additional-options")
     for detail in @survey.surveyDetails.models
       addOpts.append((new XlfSurveyDetailView(model: detail)).render().el)
+
     @
   reset: ->
     fe = @formEditorEl.empty()
-    if @survey.rows.models.length is 0
-      fe.html """
-

-          This survey is currently empty.

-          You can add questions, notes, prompts, or other fields by clicking
-          + Add question below.
-

-      """
     @survey.forEachRow (row)=>
       # row._slideDown is for add/remove animation
       $el = new XlfRowView(model: row, surveyView: @).render().$el
@@ -230,7 +246,8 @@
       if matchingRow
         @survey.rows.remove matchingRow
       # this slideUp is for add/remove row animation
-      rowEl.slideUp 175, "swing", ()=> @survey.rows.trigger "reset"
+      rowEl.slideUp 175, "swing", ()=>
+        @survey.rows.trigger "reset"
   clickInsertRow: (evt)->
     cid = $(evt.target).parents("li").eq(0).data("row-model-id")
     newIndex = @survey.rows.indexOf(@survey.rows.get(cid)) + 1
@@ -296,7 +313,7 @@
     @row = @rowView.model
     @survey = @row._parent
     ``
-  className: "cl-fixed-row cf"
+  className: "manage-list-view col-md-4"
   events:
     "click .expand-list": "expandList"
   expandList: (evt)->
@@ -356,46 +373,74 @@
         list.options = opts
         hideCl()
         row.trigger("change")
-    summ.hide()
-    exp.show()
     h2 = taVals.length * taLineH
     ta.animate({height: h2}, 275)

   render: ->
-    elHtml = ""
     numChoices = @survey.choices.models.length
     list = @row.getList()
     listName = @row.get("type").get("listName")
     editMode = @rowView.$el.find(".edit-list-view").length isnt 0

-    # aa_selectTypeBox = $("
", class: "select-type-box cl-span-2 cf").appendTo @$el
-    # aa_select = $("<select>")
-    # aa_selectTypeBox.html aa_select
-    # for [tlabel, tname] in XLF.lookupRowType.typeSelectList()
-    #   $("<option>", text: "#{tlabel}", value: tname).appendTo aa_select
-    bc_wrap = $("
", class: "bc-wrap cl-span-5 cf summarized").appendTo @$el
-    bc_wrap_hidden = $("
", class: "bc-wrap cl-span-5 cf expanded").hide().appendTo @$el
-    a_selectBox = $("
", class: "select-list-box float-left").appendTo bc_wrap
-
-    c_nListsAvailable = $("
", class: "n-lists-available").appendTo bc_wrap
-    b_selectedListSummary = $("
", class: "selected-list-summary").appendTo bc_wrap
-
-    c_nListsAvailable.text "#{numChoices} list#{if numChoices is 1 then '' else 's'} available "
+    uid = _.uniqueId("list-select-")
+
+    @$el.append """
+

+        From list:
+        <select id="#{uid}" class="form-control"></select>
+

+
+    """
+    select = @$el.find("select")
+
+    # bc_wrap = $("
", class: "bc-wrap cf summarized").appendTo @$el
+    # bc_wrap_hidden = $("
", class: "bc-wrap cf expanded").hide().appendTo @$el
+    # a_selectBox = $("
", class: "select-list-box").appendTo bc_wrap
+
+    # c_nListsAvailable = $("
", class: "n-lists-available").appendTo bc_wrap
+    # b_selectedListSummary = $("
", class: "selected-list-summary").appendTo bc_wrap
+
+    # c_nListsAvailable.text "#{numChoices} list#{if numChoices is 1 then '' else 's'} available "

     if list
+      table = $ """
+

+

+            #{list.get("name")}
+

+

+      """
+      for opt, n in list.options.models
+        tr = $("
").appendTo(table)
+        $("").text(n+".").appendTo(tr)
+        $("").text(opt.get("name")).appendTo(tr)
+      table.appendTo @$el
       opts = (opt.get("name")  for opt in list.options.models)
       optsStr = "#{opts.join(',')}"
       maxChars = 30
       if optsStr.length > maxChars
         optsStr = optsStr.slice(0,maxChars) + "..."
-      b_selectedListSummary.html "[ #{optsStr} ]"
+      # b_selectedListSummary.html "[ #{optsStr} ]"
     else
-      b_selectedListSummary.html "No list selected"
+      # b_selectedListSummary.html "No list selected"

     if numChoices is 0
       sel = $("<select>", {disabled: 'disabled'}).html($("<option>", text: "No lists available"))
-      a_selectBox.html sel
+      # a_selectBox.html sel
     else
       sel = $("<select>")
       unless list
@@ -415,8 +460,8 @@
       sel.change (evt)=>
         nextList = @survey.choices.get $(evt.target).val()
         @row.get("type").set("list", nextList)
-      a_selectBox.html sel
-    c_nListsAvailable.append """<button class='create-new-list2'>(+) Create new list</button>"""
+      # a_selectBox.html sel
+    # c_nListsAvailable.append """<button class='create-new-list2'>(+) Create new list</button>"""
     @

 class XLF.EditListView extends Backbone.View


Fix spec description

Fix spec description

--- spec_app/spec/javascripts/up/link_spec.js.coffee
+++ spec_app/spec/javascripts/up/link_spec.js.coffee
@@ -1593,7 +1593,7 @@
           next ->
             expect(jasmine.Ajax.requests.count()).toBe(0)

-        it 'never downloads a link with [rel=download] (which opens a save-as-dialog)', asyncSpec (next) ->
+        it 'never preloads a link with [rel=download] (which opens a save-as-dialog)', asyncSpec (next) ->
           $link = $fixture('a[href="/path"][up-target=".target"][up-preload][rel="download"]')

           Trigger.hoverSequence($link)


scenarios: pass source parameter on scenario creation

--- app/assets/javascripts/lib/models/scenario.coffee
+++ app/assets/javascripts/lib/models/scenario.coffee
@@ -10,6 +10,7 @@
       end_year: s.get('end_year')
       scenario_id: s.get('scenario_id')
       use_fce: s.get('use_fce')
+      source: 'ETM'

   # this method shouldn't be called as long as we keep using
   # the api_session_id fetched by the tabs_controller filter


[Prasanna] Added percentage as num since no IE support for progress bar

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -74,6 +74,7 @@
         if data.context
           progress = parseInt(data.loaded / data.total * 100, 10)
           data.context.find('.bar').css('width', progress + '%')
+          data.context.find('.percent .value').text(progress)

       done: (e, data) ->
         content = build_content_object $uploadForm, data.files[0], data.result


fixed header jumps around on ios when text field has focus

--- app/assets/javascripts/views/users_view.js.coffee
+++ app/assets/javascripts/views/users_view.js.coffee
@@ -33,10 +33,10 @@

       return false

-    @.$('#users-list').on 'focus', '.kudos-comment', ->
+    @.$('#users-list').on 'focusin', '.kudos-comment', ->
       width = $(window).width()
-      $('#dashboard').fadeOut() if width <= 768
+      $('#dashboard').css(position: 'absolute', top: 0) if width < 980

-    @.$('#users-list').on 'blur', '.kudos-comment', ->
+    @.$('#users-list').on 'focusout', '.kudos-comment', ->
       width = $(window).width()
-      $('#dashboard').fadeIn() if width <= 768+      $('#dashboard').css(position: 'fixed', top: 0) if width < 980


Deal with exceptions that don't have requests.

--- picoCTF-web/web/coffee/management-exceptions.coffee
+++ picoCTF-web/web/coffee/management-exceptions.coffee
@@ -55,9 +55,11 @@
     occurencesBadge =
     <Badge>{exception.count}</Badge>

+    apiDescription = if exception.request then {exception.request.api_endpoint_method} {exception.request.api_endpoint} else Internal Exception
+
     exceptionHeader =

-      {exception.request.api_endpoint_method} {exception.request.api_endpoint}
+      {apiDescription}

           {occurencesBadge} {time} {deleteButton}


sprites: use graphicsmagick or imagemagick instead of cairo by default

--- node_modules_koding/koding-sprites/lib/koding-sprites/sprites.coffee
+++ node_modules_koding/koding-sprites/lib/koding-sprites/sprites.coffee
@@ -8,6 +8,7 @@

 module.exports = (options, callback) ->
   options.retinaRe ?= /@2x$/
+  options.compositor ?= 'gm'

   dir = joinPath process.cwd(), options.path

@@ -18,6 +19,7 @@
       (fs.statAsync spritePath).then (stats) -> stats.isDirectory()
     .map (spritePath) ->
       generateSprites
+        compositor  : options.compositor
         src         : ["#{ spritePath }/*.png"]
         spritePath  : "#{ spritePath }.sprite.png"
         pixelRatio  : if options.retinaRe.test spritePath then 2 else 1


set the @component only if .add-on is found

--- app/assets/javascripts/anjlab/datepicker.js.coffee
+++ app/assets/javascripts/anjlab/datepicker.js.coffee
@@ -196,7 +196,11 @@
     }

     @isInput = @element.is('input');
-    @component = if @element.is('.date') then @element.find('.add-on') else false;
+
+    if @element.is('.date') && @element.find('.add-on').length > 0
+      @component = @element.find('.add-on')
+    else
+      @component = false

     if @isInput
       @element.on {


Fixed issue #2823 - Unable to open notifications (if chat is enabled).

--- app/assets/javascripts/app/controllers/chat.coffee
+++ app/assets/javascripts/app/controllers/chat.coffee
@@ -17,6 +17,7 @@
   constructor: ->
     super

+    @popovers = []
     @chatWindows = {}
     @maxChatWindows = 4
     preferences = @Session.get('preferences')
@@ -221,7 +222,12 @@
     chats

   updateMeta: =>
-    $('.popover').remove()
+
+    # clear old popovers
+    for popover in @popovers
+      popover.popover('destroy')
+    @popovers = []
+
     activeChatTopcis = @activeChatTopcis()
     @$('.js-header').html(App.view('customer_chat/chat_header')(chats: activeChatTopcis))
     @refreshElements()
@@ -244,7 +250,7 @@
       for chat in App.Chat.all()
         do (chat) =>
           @$(".js-header .js-waitingCustomers[data-chat-id=#{chat.id}] .js-badgeWaitingCustomers").text(@meta.waiting_chat_count_by_chat[chat.id])
-          @el.find(".js-waitingCustomers[data-chat-id=#{chat.id}] .js-info").popover(
+          @popovers.push @el.find(".js-waitingCustomers[data-chat-id=#{chat.id}] .js-info").popover(
             trigger:    'hover'
             html:       true
             animation:  false
@@ -258,7 +264,7 @@
           )
     else
       @badgeWaitingCustomers.text(@meta.waiting_chat_count)
-      @el.find('.js-waitingCustomers .js-totalInfo').popover(
+      @popovers.push @el.find('.js-waitingCustomers .js-totalInfo').popover(
         trigger:    'hover'
         html:       true
         animation:  false
@@ -280,7 +286,7 @@
         @addChat(session)
     @meta.active_sessions = false

-    @el.find('.js-chattingCustomers .js-info').popover(
+    @popovers.push @el.find('.js-chattingCustomers .js-info').popover(
       trigger:    'hover'
       html:       true
       animation:  false
@@ -293,7 +299,7 @@
         @chatSessionList(@meta.running_chat_session_list)
     )

-    @el.find('.js-activeAgents .js-info').popover(
+    @popovers.push @el.find('.js-activeAgents .js-info').popover(
       trigger:    'hover'
       html:       true
       animation:  false


disabled turbolinks transition cache

disabled turbolinks transition cache

--- app/assets/javascripts/turbolinks_config.js.coffee
+++ app/assets/javascripts/turbolinks_config.js.coffee
@@ -1 +1 @@
-Turbolinks.enableTransitionCache() if window['Turbolinks']+# Turbolinks.enableTransitionCache() if window['Turbolinks']


Fix function call

Fix function call

--- app/templates/coffees/background.pageaction.coffee
+++ app/templates/coffees/background.pageaction.coffee
@@ -3,7 +3,7 @@
 chrome.runtime.onInstalled.addListener (details) ->
     console.log('previousVersion', details.previousVersion)

-chrome.tabs.onUpdated.addListener(tabId) ->
+chrome.tabs.onUpdated.addListener (tabId) ->
     chrome.pageAction.show(tabId)

 console.log('\'Allo \'Allo! Event Page for Page Action')


Update hoc2016 slug to game-grove

Update hoc2016 slug to game-grove

--- app/views/play/level/modal/HeroVictoryModal.coffee
+++ app/views/play/level/modal/HeroVictoryModal.coffee
@@ -426,7 +426,7 @@
       'summits-gate': 'glacier'
     }[@level.get('slug')] ? @level.get 'campaign'
     # Return to game-dev-hoc instead if we're in that mode, since the levels don't realize they can be in that copycat campaign
-    campaign = 'game-dev-hoc' if (campaign is 'dungeon' or @level.get('slug') in ['kithgard-gates', 'hoc2016']) and storage.load('should-return-to-game-dev-hoc')
+    campaign = 'game-dev-hoc' if (campaign is 'dungeon' or @level.get('slug') in ['kithgard-gates', 'game-grove']) and storage.load('should-return-to-game-dev-hoc')
     campaign

   getNextLevelLink: (returnToCourse=false) ->


NgParse.Date initialization with moment.js

--- attributes/Date.coffee
+++ attributes/Date.coffee
@@ -8,6 +8,8 @@
                     @moment = moment options.iso, moment.ISO_8601
                 else if options.date
                     @moment = moment options.date
+                else if options.moment
+                    @moment = options.moment
                 else
                     @moment = moment()


Attach Members to Group

Attach Members to Group

--- backbone-gitlab.coffee
+++ backbone-gitlab.coffee
@@ -161,6 +161,8 @@

   @Group = @Model.extend(
     backboneClass: "Group"
+    initialize: ->
+      @members = new root.Members([],{group:@})
   )

   @Groups = @Collection.extend(


Escape & and < in span before inserting them into DOM

--- app/assets/javascripts/player/terminal_view.js.coffee
+++ app/assets/javascripts/player/terminal_view.js.coffee
@@ -45,6 +45,8 @@
     for d in data
       if d
         [char, brush] = d
+
+        char = char.replace('&', '&').replace('<', '<')

         if brush != prevBrush or i is cursorX or i is (cursorX + 1)
           if prevBrush


bind o to open enteries along with enter hotkey

--- app/assets/javascripts/rubyception/routers/log.coffee
+++ app/assets/javascripts/rubyception/routers/log.coffee
@@ -35,10 +35,9 @@
     m.bind ['j','down'] , @log.entries_index.down
     m.bind ['k','up']   , @log.entries_index.up
     m.bind String(i)    , _.bind @log.entries_index.number_hotkey, @, String(i) for i in [0..9]
-    m.bind 'o'          , @collection.open_selected
     m.bind 'shift+g'    , _.bind @log.entries_index.goto_number, @, 'bottom'
     m.bind 'g g'        , _.bind @log.entries_index.goto_number, @, 'top'
-    m.bind 'enter'      , @log.entries_index.toggle_open
+    m.bind ['o','enter'], @log.entries_index.toggle_open
   index: =>
     @log = @partial '.content', 'logs/show',
              collection: @collection


Give error if RECURLY env not set

Give error if RECURLY env not set

--- server/code/model/user.coffee
+++ server/code/model/user.coffee
@@ -42,6 +42,8 @@
 # turns the response into either an error or a javascript object,
 # both of which are passed to the specified callback.
 requestRecurlyAPI = (path, callback) ->
+  if not process.env.RECURLY_API_KEY or not process.env.RECURLY_DOMAIN
+    return callback { error: "RECURLY_API_KEY and RECURLY_DOMAIN need setting" }, null
   request.get
     uri: "https://#{process.env.RECURLY_API_KEY}:@#{process.env.RECURLY_DOMAIN}.recurly.com#{path}"
     strictSSL: true


Cleanup i18n.

Cleanup i18n.

--- app/assets/javascripts/application/i18n.js.coffee
+++ app/assets/javascripts/application/i18n.js.coffee
@@ -2,7 +2,7 @@
   category: (name) ->
     humanized = S(name).humanize().s
     for part in @uppercase
-      humanized = humanized.replace(new RegExp(part, 'gi'), S(part).toUpperCase())
+      humanized = humanized.replace(new RegExp(part, 'gi'), part.toUpperCase())
     humanized
   uppercase: [
     'api'


Start the component

Start the component

--- bin/streetsTest.coffee
+++ bin/streetsTest.coffee
@@ -66,4 +66,8 @@
         sendAndWait port, socket, 100, ->
           sendAndWait port, socket, 0, ->
             step()
-  do step
+  inst.start (err) ->
+    if err
+      console.log err
+      process.exit 1
+    do step


Use absolute path for XHR request to summary action

--- app/assets/javascripts/nav_book_summary.js.coffee
+++ app/assets/javascripts/nav_book_summary.js.coffee
@@ -1,4 +1,4 @@
 $ ->
   setInterval ->
-    $.get "books/summary"
+    $.get "/books/summary"
   , 3000


OX-308 Update texture on quality switch

OX-308 Update texture on quality switch

--- app/assets/javascripts/oxalis/model/flycam.coffee
+++ app/assets/javascripts/oxalis/model/flycam.coffee
@@ -60,10 +60,11 @@
     for i in [0..2]
       @zoomOut i

-  # Used if the user wants to explicitly set the zoom step,
-  # rather than trusting on our equation.
+  # Set offset to the best-possible zoom step
   setQuality : (value) ->
     @quality = value
+    for i in [0..2]
+      @calculateIntegerZoomStep(i)
     @hasChanged = true

   calculateIntegerZoomStep : (planeID) ->


<debug> Fat Arrow saving *this*

<debug> Fat Arrow saving *this*

--- server/models/actuator.coffee
+++ server/models/actuator.coffee
@@ -63,7 +63,7 @@
 			name: @name
 			driverId: @driverId
 		# Update DB:
-		superUpdateAttributes data, (err, actuator) ->
+		superUpdateAttributes data, (err, actuator) =>
 			if err
 				callback err, actuator
 			else


Add VisualizationModel to visualization-show in order to avoid errors in VisualizationGraph

--- app/frontend/javascripts/visualization-show.js.coffee
+++ app/frontend/javascripts/visualization-show.js.coffee
@@ -1,4 +1,5 @@
 # Imports
+VisualizationModel            = require './models/visualization.js'
 NodesCollection               = require './collections/nodes-collection.js'
 RelationsCollection           = require './collections/relations-collection.js'
 VisualizationGraph            = require './views/visualization-graph.js'
@@ -17,11 +18,13 @@
   constructor: (_id) ->
     console.log('setup visualization', _id);
     @id = _id
+    # Setup Visualization Model
+    @visualization = new VisualizationModel()
     # Setup Collections
     @nodes      = new NodesCollection()
     @relations  = new RelationsCollection()
     # Setup Views
-    @visualizationGraph = new VisualizationGraph {collection: {nodes: @nodes, relations: @relations} }
+    @visualizationGraph = new VisualizationGraph {model: @visualization, collection: {nodes: @nodes, relations: @relations} }
     # Setup Table Selector
     $('#visualization-table-selector > li > a').click @updateTable

@@ -48,6 +51,7 @@
   render: ->
     @resize()       # force resize
     # fetch collections
+    @visualization.fetch {url: '/api/visualizations/'+@id}
     @nodes.fetch {url: '/api/visualizations/'+@id+'/nodes/'}
     @relations.fetch {url: '/api/visualizations/'+@id+'/relations/'}


Add handler on wysihtml5 iframe

Add handler on wysihtml5 iframe

--- app/assets/javascripts/storytime/editor.js.coffee
+++ app/assets/javascripts/storytime/editor.js.coffee
@@ -50,6 +50,10 @@
     $('.wysihtml5-sandbox').contents().find('body').focus(->
       updateLater(10000) if $(".edit_post").length

+      $('.wysihtml5-sandbox').contents().find('body').on('keyup', ->
+        form.addClass "unsaved_changes"
+      )
+
       $('.wysihtml5-sandbox').contents().find('body').off('focus')
     )


Attempt to fix truncated log messages

Attempt to fix truncated log messages

--- spec/jasmine-helper.coffee
+++ spec/jasmine-helper.coffee
@@ -25,6 +25,7 @@
         timeReporter.logLongestSuites 10, (line) -> process.stdout.write("#{line}\n")
         process.stdout.write('\n')
         timeReporter.logLongestSpecs 10, (line) -> process.stdout.write("#{line}\n")
+        fs.closeSync(logStream)
         atom.exit(runner.results().failedCount > 0 ? 1 : 0)
   else
     AtomReporter = require './atom-reporter'


Don't show voting instructions when showing other people's rankings

--- app/assets/javascripts/views/question_page.coffee
+++ app/assets/javascripts/views/question_page.coffee
@@ -177,6 +177,7 @@
       @column2HeaderText.text("#{vote.user().fullName()}'s Ranking")
       @addAnswerButton.hide()
       @personalVote.setRelation(vote.rankings())
+      @votingInstructions.hide()
       if vote.userId() == User.currentUserId
         @personalVote.sortable('enable')
       else
@@ -206,6 +207,7 @@
     @addAnswerButton.show()
     @personalVote.sortable('enable')
     @personalVote.setRelation(Models.User.getCurrent().rankingsForQuestion(@question))
+    @updateVotingInstructions()

   updateAnswerRanking: (item) ->
     answerId = item.data('answer-id')


Unfocus spec

Unfocus spec

--- spec/list-edit-spec.coffee
+++ spec/list-edit-spec.coffee
@@ -1,6 +1,6 @@
 ListEdit = require '../lib/list-edit'

-fdescribe 'ListEdit', ->
+describe 'ListEdit', ->
   [editor, editorView] = []

   beforeEach ->


updated test

updated test

--- test/gg/pairtable-test.coffee
+++ test/gg/pairtable-test.coffee
@@ -51,9 +51,19 @@

   new gg.data.TableSet [pt1, pt2]

+createEmptyMD = ->
+  left = Table.fromArray [
+    { x: 1, y: 1, l: 1}
+    { x: 1, y: 2, l: 1}
+    { x: 2, y: 2, l: 3}
+    { x: 2, y: 3, l: 2}
+  ]
+  new gg.data.PairTable left
+
+
 checkEnsure =
   "when ensured on x,y":
-    topic: (ptable) -> ptable.ensure ['x', 'y']
+    topic: (ptable) -> ptable.clone().ensure ['x', 'y']
     "md":
       topic: (tset) -> tset.getMD()
       "has 8 rows": (md) -> assert.equal md.nrows(), 8
@@ -65,9 +75,21 @@
           else
             assert.equal p.nrows(), 4

+  "when ensured on nothing":
+    topic: (ptable) -> ptable.clone().ensure []
+    "md":
+      topic: (tset) -> tset.getMD()
+      "print": (md) ->
+        console.log md.raw()
+
+

 suite.addBatch
+  "ensure empty md":
+    _.extend({topic: createEmptyMD},
+      checkEnsure)
+###
   "ensure pairtable":
     _.extend({topic: createSimplePairTable},
       checkEnsure)
@@ -75,6 +97,7 @@
   "ensure tableset":
     _.extend({topic: createSimpleTableSet},
       checkEnsure)
+

   "pair table":
     topic: ->
@@ -154,6 +177,6 @@
         partitions = table.partition ['j1', 'j2']
         assert.equal partitions.length, 6

-
+###

 suite.export module


Removed parens in test to work around CoffeeScript bug

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -1,31 +1,31 @@
 module "Number"

 test "#abs", ->
-  equals (5).abs(), 5, "(5).abs() equals 5"
-  equals (4.2).abs(), 4.2, "(4.2).abs() equals 4.2"
+  equals 5.abs(), 5, "(5).abs() equals 5"
+  equals 4.2.abs(), 4.2, "(4.2).abs() equals 4.2"
   equals (-1.2).abs(), 1.2, "(-1.2).abs() equals 1.2"
-  equals (0).abs(), 0, "(0).abs() equals 0"
+  equals 0.abs(), 0, "(0).abs() equals 0"

 test "#ceil", ->
-  equals (4.9).ceil(), 5, "(4.9).floor() equals 5"
-  equals (4.2).ceil(), 5, "(4.2).ceil() equals 5"
+  equals 4.9.ceil(), 5, "(4.9).floor() equals 5"
+  equals 4.2.ceil(), 5, "(4.2).ceil() equals 5"
   equals (-1.2).ceil(), -1, "(-1.2).ceil() equals -1"
-  equals (3).ceil(), 3, "(3).ceil() equals 3"
+  equals 3.ceil(), 3, "(3).ceil() equals 3"

 test "#floor", ->
-  equals (4.9).floor(), 4, "(4.9).floor() equals 4"
-  equals (4.2).floor(), 4, "(4.2).floor() equals 4"
+  equals 4.9.floor(), 4, "(4.9).floor() equals 4"
+  equals 4.2.floor(), 4, "(4.2).floor() equals 4"
   equals (-1.2).floor(), -2, "(-1.2).floor() equals -2"
-  equals (3).floor(), 3, "(3).floor() equals 3"
+  equals 3.floor(), 3, "(3).floor() equals 3"

 test "#round", ->
-  equals (4.5).round(), 5, "(4.5).round() equals 5"
-  equals (4.4).round(), 4, "(4.4).round() equals 4"
+  equals 4.5.round(), 5, "(4.5).round() equals 5"
+  equals 4.4.round(), 4, "(4.4).round() equals 4"

 test "#sign", ->
-  equals (5).sign(), 1, "Positive number's sign is 1"
+  equals 5.sign(), 1, "Positive number's sign is 1"
   equals (-3).sign(), -1, "Negative number's sign is -1"
-  equals (0).sign(), 0, "Zero's sign is 0"
+  equals 0.sign(), 0, "Zero's sign is 0"

 test "#times", ->
   n = 5


Intensify end of bossfight

Intensify end of bossfight

--- app/scripts/lazerscripts/stage1/stage1boss.coffee
+++ app/scripts/lazerscripts/stage1/stage1boss.coffee
@@ -268,7 +268,7 @@
           @rocketStrikeDanceHoming()
           @sequence(
             @async @runScript(Game.Scripts.Stage1BossMine, @location())
-            @wait 2500
+            @wait 1500
           )
         )
         @wait 1000


Add test for non matching text

Add test for non matching text

--- test/routes/index_spec.coffee
+++ test/routes/index_spec.coffee
@@ -60,3 +60,16 @@
           search: word_search
           ocurrences: 1
         }, done)
+
+    it 'return 0 ocurrents if does not match search', (done) ->
+      @dummy      = 'not matching text'
+      word_search = 'lorem'
+      request(app)
+        .post('/')
+        .type('form')
+        .send(content: @dummy, search: word_search)
+        .expect('Content-Type', /json/)
+        .expect(200, {
+          search: word_search
+          ocurrences: 0
+        }, done)


test(macSpinner): Fixed spinner background color test

--- test/unit/spinner.spec.coffee
+++ test/unit/spinner.spec.coffee
@@ -38,4 +38,5 @@
       element = $compile("<mac-spinner mac-spinner-color='#123123'></mac-spinner>") $rootScope
       $rootScope.$digest()

-      expect($(".bar", element).css("background")).toBe "rgb(18, 49, 35)"
+      # FF extends background style to 'none repeat scroll 0% 0% rgb(18, 49, 35)'
+      expect($(".bar", element).css("background").indexOf("rgb(18, 49, 35)")).not.toBe -1


DefineStackView: mark current stackTemplate as inuse to fix flow logic on initial stack template creation

--- client/admin/lib/views/stacks/definestackview.coffee
+++ client/admin/lib/views/stacks/definestackview.coffee
@@ -602,5 +602,7 @@
     groupsController.setDefaultTemplate stackTemplate, (err) =>
       return  if @outputView.handleError err

+      stackTemplate.inuse = yes
+
       @emit 'Reload'
       @emit 'Completed', stackTemplate  if completed


RESTful API fix PUT=>POST for non-idempotent resource creation

--- resources/competitions.coffee
+++ resources/competitions.coffee
@@ -103,7 +103,7 @@
 start =
   route: '/'
   filters: [filters.FromJson]
-  method: 'PUT'
+  method: 'POST'
   handler: ->
     ownerId = @params.ownerId
     name = @params.name


Frontend helper updates

Frontend helper updates

HTML-Encode characters in {{inspect}}, simplify renderTime to just time.

--- server/routes/frontend.coffee
+++ server/routes/frontend.coffee
@@ -12,7 +12,17 @@

 hbs.registerHelper 'inspect', (thing, options) ->
   thing = thing or @
-  new hbs.handlebars.SafeString "
#{JSON.stringify(thing, null, 2)}
"
+
+  entities =
+    '<': '<'
+    '>': '>'
+    '&': '&'
+
+  json = JSON
+    .stringify thing, null, 2
+    .replace /[&<>]/g, (key) -> entities[key]
+
+  new hbs.handlebars.SafeString "
#{json}
"

 require('../lib/renderer')(hbs)

@@ -32,9 +42,7 @@
     now = new Date
     (now.getTime() - startTime.getTime()) + 'ms'

-  hbs.registerHelper 'renderTime', ->
-    renderTimeMS = getTime()
-    "#{req.path} rendered in #{renderTimeMS}."
+  hbs.registerHelper 'renderTime', -> getTime()

   templateData =
     adminSegment: config.buckets.adminSegment


migration to chai in process

migration to chai in process

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -31,7 +31,6 @@
 MOCHA_OPTS = {
   reporter: 'spec'
   globals: {
-    should: require 'should'
     coffee: require 'coffee-script/register'
     stack: Error.stackTraceLimit = 3
   }