nice merge

nice merge

--- resources/assets/lib/beatmap-discussions/new-discussion.coffee
+++ resources/assets/lib/beatmap-discussions/new-discussion.coffee
@@ -1,407 +0,0 @@
-# Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
-# See the LICENCE file in the repository root for full licence text.
-
-import { discussionTypeIcons } from 'beatmap-discussions/discussion-type'
-import BigButton from 'components/big-button'
-import StringWithComponent from 'components/string-with-component'
-import TimeWithTooltip from 'components/time-with-tooltip'
-import UserAvatar from 'components/user-avatar'
-import { route } from 'laroute'
-import core from 'osu-core-singleton'
-import * as React from 'react'
-import TextareaAutosize from 'react-autosize-textarea'
-import { button, div, input, label, p, i, span } from 'react-dom-factories'
-import { nominationsCount } from 'utils/beatmapset-helper'
-import { canModeratePosts, formatTimestamp, parseTimestamp, validMessageLength } from 'utils/beatmapset-discussion-helper'
-import { InputEventType, makeTextAreaHandler } from 'utils/input-handler'
-import { hideLoadingOverlay, showLoadingOverlay } from 'utils/loading-overlay'
-import { linkHtml } from 'utils/url'
-import MessageLengthCounter from './message-length-counter'
-
-el = React.createElement
-
-bn = 'beatmap-discussion-new'
-
-export class NewDiscussion extends React.PureComponent
-  constructor: (props) ->
-    super props
-
-    @disposers = new Set
-    @inputBox = React.createRef()
-    @handleKeyDown = makeTextAreaHandler @handleKeyDownCallback
-
-    @state =
-      cssTop: null
-      message: @storedMessage()
-      timestampConfirmed: false
-      posting: null
-
-
-  componentDidMount: =>
-    @setTop()
-    $(window).on 'resize', @setTop
-
-    if @props.autoFocus
-      @disposers.add(core.reactTurbolinks.runAfterPageLoad => @inputBox.current?.focus())
-
-
-  componentDidUpdate: (prevProps) =>
-    if prevProps.beatmapset.id != @props.beatmapset.id
-      @setState message: @storedMessage()
-      return
-
-    @storeMessage()
-
-
-  componentWillUnmount: =>
-    $(window).off 'resize', @setTop
-    @postXhr?.abort()
-    @disposers.forEach (disposer) => disposer?()
-
-
-  render: =>
-    cssClasses = 'beatmap-discussion-new-float'
-    cssClasses += ' beatmap-discussion-new-float--pinned' if @props.pinned
-
-    div
-      className: cssClasses
-      style:
-        top: @state.cssTop
-      div className: 'beatmap-discussion-new-float__floatable',
-        div
-          className: 'beatmap-discussion-new-float__content'
-          ref: @props.innerRef
-
-          @renderBox()
-
-
-  renderBox: =>
-    canHype =
-      @props.beatmapset.current_user_attributes?.can_hype &&
-      @props.beatmapset.can_be_hyped &&
-      @props.mode == 'generalAll'
-
-    canPostNote =
-      @props.currentUser.id == @props.beatmapset.user_id ||
-      (@props.currentUser.id == @props.currentBeatmap.user_id && @props.mode in ['general', 'timeline']) ||
-      @props.currentUser.is_bng ||
-      canModeratePosts(@props.currentUser)
-
-    buttonCssClasses = 'btn-circle'
-    buttonCssClasses += ' btn-circle--activated' if @props.pinned
-
-    div
-      className: 'osu-page osu-page--small'
-      div
-        className: bn
-        div className: "page-title",
-          osu.trans('beatmaps.discussions.new.title')
-
-          span className: 'page-title__button',
-            span
-              className: buttonCssClasses
-              onClick: @toggleSticky
-              title: osu.trans("beatmaps.discussions.new.#{if @props.pinned then 'unpin' else 'pin'}")
-
-              span className: 'btn-circle__content',
-                i className: 'fas fa-thumbtack'
-
-        div className: "#{bn}__content",
-          div
-            className: "#{bn}__avatar"
-            el UserAvatar, user: @props.currentUser, modifiers: ['full-rounded']
-
-          div className: "#{bn}__message", id: 'new',
-            if @props.currentUser.id?
-              [
-                el TextareaAutosize,
-                  key: 'input'
-                  disabled: @state.posting? || !@canPost()
-                  className: "#{bn}__message-area js-hype--input"
-                  value: if @canPost() then @state.message else ''
-                  onChange: @setMessage
-                  onKeyDown: @handleKeyDown
-                  onFocus: @onFocus
-                  ref: @inputBox
-                  placeholder: @messagePlaceholder()
-
-                el MessageLengthCounter,
-                  key: 'counter'
-                  message: @state.message
-                  isTimeline: @isTimeline()
-              ]
-            else
-              osu.trans('beatmaps.discussions.require-login')
-
-        div className: "#{bn}__footer",
-          div
-            className: "#{bn}__footer-content js-hype--explanation js-flash-border"
-            style:
-              opacity: 0 if @props.mode != 'timeline' && !(@props.mode == 'generalAll' && @props.beatmapset.can_be_hyped)
-            div
-              key: 'label'
-              className: "#{bn}__timestamp-col #{bn}__timestamp-col--label"
-              if @props.mode == 'timeline'
-                osu.trans 'beatmaps.discussions.new.timestamp'
-              else # mode == 'generalAll'
-                osu.trans 'beatmaps.hype.title'
-            div
-              key: 'timestamp'
-              className: "#{bn}__timestamp-col"
-              if @props.mode == 'timeline'
-                if @timestamp()?
-                  formatTimestamp @timestamp()
-                else
-                  osu.trans 'beatmaps.discussions.new.timestamp_missing'
-              else if @props.beatmapset.can_be_hyped # mode == 'generalAll'
-                if @props.currentUser.id?
-                  el React.Fragment, null,
-                    if @props.beatmapset.current_user_attributes.can_hype
-                      osu.trans 'beatmaps.hype.explanation'
-                    else
-                      @props.beatmapset.current_user_attributes.can_hype_reason
-
-                    if @props.beatmapset.current_user_attributes.can_hype || @props.beatmapset.current_user_attributes.remaining_hype <= 0
-                      el React.Fragment, null,
-                        el StringWithComponent,
-                          mappings:
-                            remaining: @props.beatmapset.current_user_attributes.remaining_hype
-                          pattern: " #{osu.trans 'beatmaps.hype.remaining'}"
-                        if @props.beatmapset.current_user_attributes.new_hype_time?
-                          el StringWithComponent,
-                            mappings:
-                              new_time: el TimeWithTooltip,
-                                dateTime: @props.beatmapset.current_user_attributes.new_hype_time
-                                relative: true
-                            pattern: " #{osu.trans 'beatmaps.hype.new_time'}"
-                else
-                  osu.trans 'beatmaps.hype.explanation_guest'
-          div
-            className: "#{bn}__footer-content #{bn}__footer-content--right"
-            if canHype
-              @submitButton 'hype'
-            if canPostNote
-              @submitButton 'mapper_note'
-            @submitButton 'praise'
-            @submitButton 'suggestion'
-            @submitButton 'problem'
-
-        if @nearbyDiscussions().length > 0
-          currentTimestamp = formatTimestamp @timestamp()
-          timestamps =
-            for discussion in @nearbyDiscussions()
-              linkHtml BeatmapDiscussionHelper.url(discussion: discussion),
-                formatTimestamp(discussion.timestamp)
-                classNames: ['js-beatmap-discussion--jump']
-          timestampsString = osu.transArray(timestamps)
-
-          div className: "#{bn}__notice",
-            div
-              className: "#{bn}__notice-text"
-              dangerouslySetInnerHTML:
-                __html: osu.trans 'beatmap_discussions.nearby_posts.notice',
-                  timestamp: currentTimestamp
-                  existing_timestamps: timestampsString
-
-            label className: "#{bn}__notice-checkbox",
-              div className: 'osu-switch-v2',
-                input
-                  className: 'osu-switch-v2__input'
-                  type: 'checkbox'
-                  checked: @state.timestampConfirmed
-                  onChange: @toggleTimestampConfirmation
-                span className: 'osu-switch-v2__content'
-
-              osu.trans('beatmap_discussions.nearby_posts.confirm')
-
-
-  canPost: =>
-    !@props.currentUser.is_silenced &&
-    (!@props.beatmapset.discussion_locked || canModeratePosts(@props.currentUser)) &&
-    (!@props.currentBeatmap.deleted_at? || @props.mode == 'generalAll')
-
-
-  cssTop: (sticky) =>
-    return if !sticky || !@props.stickTo?.current?
-    core.stickyHeader.headerHeight + @props.stickTo.current.getBoundingClientRect().height
-
-
-  handleKeyDownCallback: (type, event) =>
-    # Ignores SUBMIT, requiring shift-enter to add new line.
-    switch type
-      when InputEventType.Cancel
-        @setSticky(false)
-
-
-  isTimeline: =>
-    @props.mode == 'timeline'
-
-
-  messagePlaceholder: =>
-    if @canPost()
-      osu.trans "beatmaps.discussions.message_placeholder.#{@props.mode}", version: @props.currentBeatmap.version
-    else
-      if @props.currentUser.is_silenced
-        osu.trans 'beatmaps.discussions.message_placeholder_silenced'
-      else if @props.beatmapset.discussion_locked
-        osu.trans 'beatmaps.discussions.message_placeholder_locked'
-      else
-        osu.trans 'beatmaps.discussions.message_placeholder_deleted_beatmap'
-
-
-  nearbyDiscussions: =>
-    return [] if !@timestamp()?
-
-    if !@nearbyDiscussionsCache? || (@nearbyDiscussionsCache.beatmap != @props.currentBeatmap || @nearbyDiscussionsCache.timestamp != @timestamp())
-      @nearbyDiscussionsCache =
-        beatmap: @props.currentBeatmap
-        timestamp: @timestamp()
-        discussions: BeatmapDiscussionHelper.nearbyDiscussions(@props.currentDiscussions.timelineAllUsers, @timestamp())
-
-    @nearbyDiscussionsCache.discussions
-
-
-  onFocus: =>
-    @setSticky true
-
-
-  post: (e) =>
-    return if !@validPost() || @postXhr?
-
-    type = e.currentTarget.dataset.type
-
-    if type == 'problem'
-      problemType = @problemType()
-
-      if problemType != 'problem'
-        return unless confirm(osu.trans("beatmaps.nominations.reset_confirm.#{problemType}"))
-
-    if type == 'hype'
-      return unless confirm(osu.trans('beatmaps.hype.confirm', n: @props.beatmapset.current_user_attributes.remaining_hype))
-
-    showLoadingOverlay()
-    @setState posting: type
-
-    data =
-      beatmapset_id: @props.currentBeatmap.beatmapset_id
-      beatmap_discussion:
-        message_type: type
-        timestamp: @timestamp()
-        beatmap_id: @props.currentBeatmap.id unless @props.mode == 'generalAll'
-      beatmap_discussion_post:
-        message: @state.message
-
-    @postXhr = $.ajax route('beatmapsets.discussions.posts.store'),
-      method: 'POST'
-      data: data
-
-    .done (data) =>
-      @setState
-        message: ''
-        timestampConfirmed: false
-
-      $.publish 'beatmapDiscussionPost:markRead', id: data.beatmap_discussion_post_id
-      $.publish 'beatmapsetDiscussions:update', beatmapset: data.beatmapset
-
-    .fail osu.ajaxError
-
-    .always =>
-      hideLoadingOverlay()
-      @postXhr = null
-      @setState posting: null
-
-
-  problemType: =>
-    canDisqualify = currentUser.is_admin || currentUser.is_moderator || currentUser.is_full_bn
-    willDisqualify = @props.beatmapset.status == 'qualified'
-
-    return 'disqualify' if canDisqualify && willDisqualify
-
-    canReset = currentUser.is_admin || currentUser.is_nat || currentUser.is_bng
-    currentNominations = nominationsCount(@props.beatmapset.nominations, 'current')
-    willReset = @props.beatmapset.status == 'pending' && currentNominations > 0
-
-    return 'nomination_reset' if canReset && willReset
-
-    return 'problem_warning' if willDisqualify
-
-    'problem'
-
-
-  setMessage: (e) =>
-    @setState message: e.currentTarget.value
-
-
-  # TODO: to whoever refactors this - this 'sticky' behaviour was ported to new-review.tsx, so remember to refactor that too
-  setSticky: (sticky = true) =>
-    @setState
-      cssTop: @cssTop(sticky)
-
-    @props.setPinned?(sticky)
-
-
-  setTop: =>
-    @setState
-      cssTop: @cssTop(@props.pinned)
-
-
-  storageKey: =>
-    "beatmapset-discussion:store:#{@props.beatmapset.id}:message"
-
-
-  storeMessage: =>
-    if @state.message == ''
-      localStorage.removeItem @storageKey()
-    else
-      localStorage.setItem @storageKey(), @state.message
-
-
-  storedMessage: =>
-    localStorage.getItem(@storageKey()) ? ''
-
-
-  submitButton: (type, extraProps) =>
-    typeText = if type == 'problem' then @problemType() else type
-
-    el BigButton,
-      key: type
-      disabled: !@validPost() || @state.posting? || !@canPost()
-      icon: discussionTypeIcons[type]
-      isBusy: @state.posting == type
-      modifiers: 'beatmap-discussion-new'
-      text: osu.trans("beatmaps.discussions.message_type.#{typeText}")
-      props: _.merge
-        'data-type': type
-        onClick: @post
-        extraProps
-
-
-  timestamp: =>
-    return unless @props.mode == 'timeline'
-
-    if @timestampCache?.message != @state.message
-      @timestampCache = null
-
-    if !@timestampCache?
-      @timestampCache =
-        message: @state.message
-        timestamp: parseTimestamp(@state.message)
-
-    @timestampCache.timestamp
-
-
-  toggleSticky: =>
-    @setSticky(!@props.pinned)
-
-
-  toggleTimestampConfirmation: =>
-    @setState timestampConfirmed: !@state.timestampConfirmed
-
-
-  validPost: =>
-    return false if !validMessageLength(@state.message, @isTimeline())
-
-    if @isTimeline()
-      @timestamp()? && (@nearbyDiscussions().length == 0 || @state.timestampConfirmed)
-    else
-      true


Updated instagram script

Updated instagram script

--- src/instagram.coffee
+++ src/instagram.coffee
@@ -28,7 +28,7 @@
 module.exports = (robot) ->
   robot.respond /(insta tag)( me )?(.*)/i, (msg) ->
     count = 1
-    authenticateUser()
+    authenticateUser(msg)
     if msg.match[3]
       text = msg.match[3].trim()
       text = text.split(" ")
@@ -47,7 +47,7 @@

   robot.respond /(insta user)( me )?(.*)/i, (msg) ->
     count = 1
-    authenticate_user()
+    authenticateUser(msg)
     if  msg.match[3]
       text = msg.match[3].trim()
       text = text.split(" ")
@@ -73,7 +73,7 @@

-authenticateUser = () ->
+authenticateUser = (msg) ->
   unless config.client_key
     msg.send "Please set the HUBOT_INSTAGRAM_CLIENT_KEY environment variable."
     return


Repaired the click cancelling and draggable cleanup logic.

--- src/draggable.coffee
+++ src/draggable.coffee
@@ -120,6 +120,9 @@
     handleElementMouseDown: (e) =>
       @cancelAnyScheduledDrag()

+      # Until told otherwise, the interaction started by this mousedown should not cancel any subsequent click event
+      @shouldCancelClick = false
+
       # Bail if this is not a valid handle
       return unless @isValidHandle(e.target)

@@ -135,18 +138,8 @@
       false

     handleElementClick: (e) =>
-      # Clicks should be passed through if this interaction didn't result in a drag
-      shouldPermitClick = not @dragStarted
-
-      # Clean up
-      @dragStarted = false
-      @$helper = null
-      @helperStartPosition = {}
-      @elementStartDocumentOffset = {}
-      @helperStartDocumentOffset = {}
-      delete @mousedownEvent
-
-      unless shouldPermitClick
+      # Clicks should be cancelled if the last mousedown/mouseup interaction resulted in a drag
+      if @shouldCancelClick
         # Cancel the click
         e.stopImmediatePropagation()
         false
@@ -156,6 +149,9 @@
       $(document).off
         mousemove: @handleMouseMove
         mouseup: @handleMouseUp
+
+      # Lest a click event follow this mouseup, decide whether it should be permitted or not
+      @shouldCancelClick = !!@dragStarted

       return unless @dragStarted

@@ -170,6 +166,9 @@

       # Trigger the stop event
       @handleDragStop(e)
+
+      # Clean up
+      @cleanUp()

     handleDocumentMouseMove: (e) =>
       # Trigger the start event, once
@@ -293,6 +292,15 @@
         # Attach it to the body
         .appendTo('body')

+    cleanUp: ->
+      # Clean up
+      @dragStarted = false
+      @$helper = null
+      @helperStartPosition = {}
+      @elementStartDocumentOffset = {}
+      @helperStartDocumentOffset = {}
+      delete @mousedownEvent
+
   $.fn.draggable = (options) ->
     this.each ->
       unless $(this).data('draggable')?


AdminMembersView: Make blocked users only available to Koding group.

--- client/admin/lib/views/members/adminmembersview.coffee
+++ client/admin/lib/views/members/adminmembersview.coffee
@@ -1,5 +1,6 @@
 kd                    = require 'kd'
 KDView                = kd.View
+isKoding              = require 'app/util/isKoding'
 KDTabView             = kd.TabView
 KDTabPaneView         = kd.TabPaneView
 TeamMembersCommonView = require './teammemberscommonview'
@@ -25,12 +26,14 @@
     tabView.addPane allMembersPane = new KDTabPaneView name: 'All Members'
     tabView.addPane adminsPane     = new KDTabPaneView name: 'Admins'
     tabView.addPane moderatorsPane = new KDTabPaneView name: 'Moderators'
-    tabView.addPane blockedMembersPane = new KDTabPaneView name: 'Blocked'

     allMembersPane.addSubView  new TeamMembersCommonView { fetcherMethod: 'fetchMembers'    }, data
     adminsPane.addSubView      new TeamMembersCommonView { fetcherMethod: 'fetchAdmins'     }, data
     moderatorsPane.addSubView  new TeamMembersCommonView { fetcherMethod: 'fetchModerators' }, data
-    blockedMembersPane.addSubView new GroupsBlockedUserView {}, data
+
+    if isKoding()
+      tabView.addPane blockedMembersPane = new KDTabPaneView name: 'Blocked'
+      blockedMembersPane.addSubView new GroupsBlockedUserView {}, data

     tabView.showPaneByIndex 0
     @addSubView tabView


StacksCustomViews: repoList implementation basic requirements are done

--- client/admin/lib/views/stacks/stackscustomviews.coffee
+++ client/admin/lib/views/stacks/stackscustomviews.coffee
@@ -11,6 +11,8 @@
 showError                       = require 'app/util/showError'
 applyMarkdown                   = require 'app/util/applyMarkdown'

+GitHub                          = require 'app/extras/github/github'
+RepoItem                        = require 'app/extras/github/views/repoitem'
 CustomViews                     = require 'app/commonviews/customviews'
 IDEEditorPane                   = require 'ide/workspace/panes/ideeditorpane'
 CredentialListItem              = require './credentiallistitem'
@@ -412,6 +414,8 @@
           steps           : STEPS.REPO_FLOW
           selected        : 2
         text              : "Github: Select a repository from your account"
+        repoList          :
+          username        : 'gokmen'
         navCancelButton   :
           title           : '< Select another provider'
           callback        : cancelCallback
@@ -440,6 +444,29 @@
         callback data

       return container
+
+
+    repoList: (options) =>
+
+      { username } = options
+
+      controller          = new kd.ListViewController
+        viewOptions       :
+          type            : 'github'
+          wrapper         : yes
+          itemClass       : RepoItem
+          itemOptions     :
+            buttonTitle   : 'select'
+        noItemFoundWidget : new kd.View
+          cssClass        : 'noitem-warning'
+          partial         : 'There is no repository to show.'
+
+      GitHub.fetchUserRepos username, (err, repos) =>
+        return  if showError err
+        controller.replaceAllItems repos
+
+      __view = controller.getView()
+      return { __view, controller }

     credentialList: (options) =>


Fixed undefined reference.

Fixed undefined reference.

--- api/users.coffee
+++ api/users.coffee
@@ -41,8 +41,8 @@
                 message: 'Aucune équipe n\'a été spécifiée'
             ]
             return
-        User.count where: teamId: value
-        .then (ammount) ->
+        User.count where: teamId: req.body.teamId
+        .then (amount) ->
             if amount and amount >= 10
                 res.validationError fields: [
                     path: 'teamId'


Make it simple

Make it simple

--- client/landing/app/MainApp/kodingappscontroller.coffee
+++ client/landing/app/MainApp/kodingappscontroller.coffee
@@ -429,11 +429,12 @@
           orderedBlocks.push blockOptions

       if app.devMode
-        appDevModePath = "/Users/#{nickname}/Sites/#{nickname}.koding.com/website/.applications/#{__utils.slugify name}"
+        appResourceRoot = "/Users/#{nickname}/Sites/#{nickname}.koding.com/website/.applications"
+        appDevModePath  = "#{appResourceRoot}/#{__utils.slugify name}"

         asyncStack.push (cb)=>
           @kiteController.run "rm -rf #{escapeFilePath appDevModePath}", =>
-            @kiteController.run "mkdir /Users/#{nickname}/Sites/#{nickname}.koding.com/website/.applications", =>
+            @kiteController.run "test -d #{appResourceRoot} || mkdir #{appResourceRoot}", =>
               @kiteController.run "ln -s #{escapeFilePath getAppPath app} #{escapeFilePath appDevModePath}", -> cb()

       orderedBlocks.forEach (block)=>


Cleanup AppStorage for KodingApps even after failed fetchFromFS

--- client/landing/app/MainApp/kodingappscontroller.coffee
+++ client/landing/app/MainApp/kodingappscontroller.coffee
@@ -91,6 +91,7 @@
     # require ["coffee-script"], (coffee)=>
     @kiteController.run "ls #{escapeFilePath path} -lpva", (err, response)=>
       if err
+        @putAppsToAppStorage {}
         warn err
         callback err
       else
@@ -162,8 +163,8 @@
     @constructor.manifests = {}
     KDApps = {}
     @fetchAppsFromFs (err, apps)=>
+      @emit "AppsRefreshed", apps
       if not err
-        @emit "AppsRefreshed", apps
         callback? err, apps
       else
         callback err


feat: enable image in template

feat: enable image in template

--- client/src/daily-report/services/report-builder.coffee
+++ client/src/daily-report/services/report-builder.coffee
@@ -10,30 +10,47 @@
 )->
   converter = new showdown.Converter()

+  renderImages = (message, useCid) ->
+    images = message.body.match(/data:image\/(.*);base64,([^"]*)/gm)
+
+    if images.length > 0
+      for imageIdx of images
+        match = /data:image\/(.*);base64,(.*)$/.exec(images[imageIdx])
+
+        if match != null
+          imageBase64 = match[0]
+          imageType = match[1]
+
+          src = if useCid then "cid:image#{imageIdx}" else imageBase64
+
+          message.body = message.body.replace(imageBase64, "#{src}")
+
+          if useCid
+            message.cids = (message.cids || []).concat([{
+              type: "image/#{imageType}"
+              name: "Image #{imageIdx}"
+              base64: imageBase64.split(',')[1]
+              id: "image#{imageIdx}"
+            }])
+
+    return message
+
   renderBDC = (message, svg, useCid) ->
     bdc.getPngBase64(svg)
     .then (bdcBase64) ->
       src = if useCid then 'cid:bdc' else bdcBase64

-      base64bis = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAIAAAD6CAYAAAA7ruXSAAAgAElEQVR4Xu2dCbxd49X/VyKDDBISEtWKlwqtmHVSipqqVUVrVoqqWSSoEsQQMUVCYkpRs5hLhQ6m11S0WqkSWqmhMdMbCRkkuUn+Z+2+5/5Pbu6593n2Xvuufc79ns+nn7a5a63n2d/13L3W8zvP3rfTktJH+EAAAhCAAAQgAAEIQAACEIAABCDQIQh0QgjoEHnmIiEAAQhAAAIQgAAEIAABCEAAAgkBhAAWAgQgAAEIQAACEIAABCAAAQhAoAMRQAjoQMnmUiEAAQhAAAIQgAAEIAABCEAAAggBrAEIQAACEIAABCAAAQhAAAIQgEAHIoAQ0IGSzaVCAAIQgAAEIAABCEAAAhCAAAQQAlgDEIAABCAAAQhAAAIQgAAEIACBDkQAIaADJZtLhQAEIAABCEAAAhCAAAQgAAEIIASwBiAAAQhAAAIQgAAEIAABCEAAAh2IAEJAB0o2lwoBCEAAAhCAAAQgAAEIQAACEEAIYA1AAAIQgAAEIAABCEAAAhCAAAQ6EAGEAKdkz5gxQ+bOnStf+MIXnGaw9LCNjY3JfPr06ZNqPvPnz5cPPvhABg0alMq/Vp3efvtt6dWrl6y00kq1egnMGwIQgAAEIAABCEAAAhDoYAQQApwSftddd8nChQtl3333dZnBggULpFu3bk1jZ53P3/72N3nggQfk1FNPTXU9zeeTKoiD0+jRo2WnnXaSzTbbzGF0hoQABCAAAQhAAAIQgAAEIBBPIBch4LTTTpP//Oc/0rlzZ+nXr5989atflR/84AfSqVOn+BnWqUfWjXcWLH/5y1/k8ccflxNOOKEQQkBL82l+fYsXL5Ybb7xRPvvsMzniiCOafvzee+/JtddeK++++66sv/76cvDBB8vyyy+fBU+UL0JAFC6MIQABCEAAAhCAAAQgAIECEMhFCDj66KPluOOOk8997nPy73//W2644QbZdttt5bvf/W4BLrkYU/AUAv70pz/Jk08+KSeeeGIhhICW5lOZpenTp8vtt98u77zzjnzpS19qEgKWLFkiZ599tqy33nrJ+tJ1pmuuPU9ZIAQU4/eJWUAAAhCAAAQgAAEIQAAC4QRyEwJOPvlkWX311ZOZ6Cbu448/TjZwzTd9jzzyiEybNq1pc/fQQw/JH/7wh+Sb3xVXXFF++MMfyqabbiqvvPKKTJo0SRoaGmTAgAFyxhlnLHXCoCw46EmErl27ygYbbCB77bWX9OzZM5nDlClT5J577pFPP/00iaebxS5dusif//xnuf/++5O4enph1KhRTfR0o3zIIYckG039+emnny4TJkxI/J544gn561//KsOHD0+erb/11lvlxRdfTOa83377yTrrrNPquJVCgDL5zW9+IyNGjBA9Iv+rX/1K3nzzzWTuw4YNk89//vNLZfTDDz+Um2++ORFZ9Gc6nr5rQJ/zHzt2rOjP582bl/zboYcemvCq/Oh4+i16+XPuuefK//7v/8qzzz6bjK8nOXbYYQfZeeedE5PWrq8co/mjAVdddVXCo3v37rLlllvKbrvtlpiW86vz+8Y3viEHHHBAsiaaz6d///5N89PYmjddE6+99lrTWtHn83Xul1xySfKYwz//+U+58sor5eKLL25aG3piQMWC8ePHJzZPP/108p+yCHL11VfLaqutllyrniq45ZZb5K233pI111xTDjrooOTZfxUc9LEHPUWhbL7zne8kwoN+KoWAm266ST755BM56qij5B//+Eer6zX8VxRLCEAAAhCAAAQgAAEIQAACtgRyFwJmzZqVbMw233zzZAPVmhCgm/I77rgj2byuuuqqohurwYMHJ89g68ZfN19f+9rXElFBN2+Vn7///e+Jr27MdZN52223Se/evZNN4/vvv59s2A477LDE7/LLL5dvfetb8u1vf1t0Q67xVDTQF95Vbpp1M6sbbd0k6tx0g/6LX/xC1lprreTb55VXXjn5mW5idcz9999fdNP629/+NtmgqijR2rj6jgDdDOt8dN46looduhnfc889Zfbs2clGtPJZ/kWLFslZZ50lG264oWy99dbJvHSDes455yQb1qFDhybP6ffo0UPuu+8+mTNnTvJvzYUAPRGgY+pnueWWSzh89NFHsvfeeyf/fdlll8nxxx+fbIirXZ8KItWEAN2AK3/dXKt4otz0Wk466SQ55ZRTEnZ6fcq7vCYq59PSMm8uGum16wZdeehH4+njDueff/5SL+/TfzvyyCNl7bXXTgQUHU+FAd3UKyvd8Ot1nnnmmUk+ttpqq4SH5kHXzx//+Mckp8ccc0wiiqivXo/mqywE6Dp/6qmnkn9X8aOt9Wr7a0w0CEAAAhCAAAQgAAEIQAAC4QRyEwJ0M6TvBNCNqG4AdfOnb6RvTQhQwUC/fVfBQD+68dbNlgoB5513XrIB33333ZfaGJcvVYWAX//618lmTj+6mdV3FeimTTeQ+u3x4YcfnvxMv/1++eWXRR9haO2Ivm6wdWOvjznoBv1f//pXImjot+W6gdTTArqB1A2ijlUWJ3QDrd/k6zfirY2rAoUee//JT34iQ4YMSeZ29913yxtvvCE/+9nPpG/fvstkUudwxRVXyJgxY5INvH507D322EO+/OUvJ5t+PRWgm3CNPW7cuOQb88pPyKMBKk7oMXwVS6pdX+VfCGjtZYEXXXRRsrnWZ/h//vOfJ9er740ovzOirUcDynNvLgToxltPZuhJCv2osKJzVWFAhaTyRwUdPZ2ij6aU14e+S0DX48iRIxOhSpn/8pe/TLjqvPQxhAsuuCARMZShniLZZpttkpATJ06UL37xi8k6UCFA864nAFQE0FMl+mlrvS6TWP4BAhCAAAQgAAEIQAACEIBAOxHITQg49thjk+e19Uj3ww8/nBx1129J9VvcyufTKzd35Q3txhtvnFx+pRCgf5pOHzHQDZtu6HbcccelEDUXAvSH+i2wbvR0fP1Wt7xx1m/OdROrjy+0JgSomKDHynWjqI8MqAihG089iq/fwOsGUY+C6waw8ttxfbGdigz6OEJr4+rP9KP+5Y2rHn9XMUA3x1/5yleSb+hVVCl/lJ8+OqGPKZQ/Or+NNtpItthii6WEAP0ThSrA6Aa38hMiBOiJBz2er8f6q12fbuzLn0ohQE8taK6ef/755HEF/f/6KMY3v/lNeemllxLm+m96bRojrRCgY+rjHs1PBGhe9E/6lT/KWbmpcKMnHZSVPj6ywgorJHNU8aD8eEJlHnWOKgToGlCWZeFC18/222+fPLaiQoCe/NCTDSoAlXPV1nptp99vhoEABCAAAQhAAAIQgAAEILAMgdyEgMp3BOgxcf0mVjes+o22bgR1c6WfSiFAv43Vo/965F0/lUJAeeYqBOi3/LrJX3fddZsuqLkQMHPmzGQDq5tCHUM3a7oRbP5p66V9Ou/tttsuES/0WLvG1BMK+m37T3/602RDq5t+FQpWWWWVpcLr0fy2xtWNo75rQL/Vrty86vz1W3l9BGCXXXZpiqvvU9Dn4ENOBOgjD5qH5kKAvqVfmei1lD/NOegb+vUkx/e+972q11d5sZVCwDPPPCMPPvhgsjHWa9ITCXrkXoUA/ehGWr/NV7FA86O5az6fln5Xm58I0HWl3CvfEaDf/uuJiMqPijXK90c/+lGyoVchQN/JoEKAnqLQx0T0G/3rrrsuOQXQ/KOs9SSIiiLNPyoE6PpQsUHf6aCPElT+dYxq65V7EQQgAAEIQAACEIAABCAAAS8CuQkBekRdTwToZlSfr9Zj8PrNrW7KdHOqL4nTTdhjjz2W/Ew3ULp51G/cdZOv38yqEKBHsnXjrQKCHsHW5/h1Y6bP9JdPDig83Uzqt9j6HgDdwN57773J89x6RF83jPrMvh5J1yPd+u96LFw3gm0JAbqZ10cJ9BtgfR+AHgvXl9IdeOCBsskmmyR50xfO6bz0G2L903X6rLqeOAgdVzf8er366IIKDPpIgH5jrZtxPWqu11r+NH9HwHPPPZcwrHxHQPnRgGpCgL5kUG30mL5uWnUMzZEerS+/cb8sBKgIUe36KhetbqQ1X3oC44UXXkg29roG9BGFSy+9NBEC9ISDPiqhj3uUj96rOKQnL5rPp/ySx8oxmgsB+jPNqz77r5vx66+/XtZYY43kMYnmH308QcfUF/mpvYoger0qSOk60JMLesri61//eiIM6KkO/c/AgQOTdzDoSw51/ajYo+KOxtBP+R0BupZVlNDHWvQRgrbWq9cvPONCAAIQgAAEIAABCEAAAhDIRQjQ5+f1Lfv60W+EddOk38aWX8Sn3xjrBltfsKYbRX1+/Pvf/36yGdMX/unmVl+QV35Du26s9AVwuknWf1dxQF/MVz7qXxYC9BtdFQHKGzXdrOtb/PWj31jrmHpkW0UA/TZf59WWEKBv4NcNom5wdQOrR8z1fQG6sSwfI1dhQV9OqO8d0BfM6V8s0Gf8Q8fVxyd0DD2xoCKDiiH60fcP6LPsek2Vn/JfDdDHLXROykL/QoCKEZXvCKgmBGgsfWmeclbhQjfH+r+rCQGtXV95XuqrG37l/eMf/zjZlOtjAJpT5a1z1E21bvx1Xip2VL59v/l8dEPf/NOSEKBiiwoQ+t/6ngVlqNfU/KMnEFQcKr8DQMfT0wGVL1LUGPrXH5SrCjF6GkIFBj3BoEKJfuuva1bFAH3kQk9zVP7VAM2/CkUqCOipjdbWK7ceCEAAAhCAAAQgAAEIQAACXgRyEQLSXoxuiHVDqRs5FRJ0M6Xfkre0KWw+RkvvCEg7D/wgAAEIQAACEIAABCAAAQhAAAL1SqBQQoB+237nnXcmf2lAj6vrW9nL7wtoKwEIAW0R4ucQgAAEIAABCEAAAhCAAAQgAAGRQgkBJAQCEIAABCAAAQhAAAIQgAAEIACBfAkgBOTLl+gQgAAEIAABCEAAAhCAAAQgAIFCEUAIKFQ6mAwEIAABCEAAAhCAAAQgAAEIQCBfAggB+fIlOgQgAAEIQAACEIAABCAAAQhAoFAEEAIKlQ4mAwEIQAACEIAABCAAAQhAAAIQyJcAQkC+fIkOAQhAAAIQgAAEIAABCEAAAhAoFAGEgEKlg8lAAAIQgAAEIAABCEAAAhCAAATyJYAQkC9fokMAAhCAAAQgAAEIQAACEIAABApFACGgUOlgMhCAAAQgAAEIQAACEIAABCAAgXwJIATky5foEIAABCAAAQhAAAIQgAAEIACBQhFACChUOpgMBCAAAQhAAAIQgAAEIAABCEAgXwIIAfnyJToEIAABCEAAAhCAAAQgAAEIQKBQBBACCpUOJgMBCEAAAhCAAAQgAAEIQAACEMiXAEJAvnyJDgEIQAACEIAABCAAAQhAAAIQKBQBhIBCpYPJQAACEIAABCAAAQhAAAK1RqCxsVF+9KMfSY8ePeS2226rtekz3w5IACGgAyadS4YABCAAAQhAAAIQgAAE7AgMHTpUFi1aJA0NDQgBdliJlCMBhIAc4RIaAhCAAAQgAAEIQAACEKhvAvfdd588+eSTsvPOO8vEiRMRAuo73XVzdQgBdZNKLgQCEIAABCAAAQhAAAIQaE8C8+bNkx133FEeeughefbZZxEC2hM+Y2UigBCQCR/OEIAABCAAAQhAAAIQgEBHJfDggw/KKaecIqussorMmDFD3nrrLRk/frzstddeHRUJ110jBBACaiRRTBMCEIAABCAAAQhAAAIQKC6Bxx57jBMBxU0PM2tGACHAeUnMnz9fPvjgAxk0aJDzTDre8Mpd3+zap0+fjnfxzlc8bdo0GTx4sPMsOt7ws2bNEr3nDBgwoONdvPMVT58+XQYOHCjdu3d3nknHGp4a65dvaqwfe2qsD3utsY8++qjcfvvtvCOgnVNAjU0HHCEgHTczL5oUM5TRgWhSopGZOdCkmKGMCoQQEIXL1JgmxRRncDBqbDAqc0NqrDnS4IDU2GBUpobUWFOcUcGosVG4mowLJwTon90YO/5C6datmww75oR0V1VDXjQpfsmiSfFjT5Piw54mxYe7jkqT4sOeGuvDXUelxvqxp8b6sKfG+nCnxqbnXjgh4Nobr5HFixfL7NmfIgSkzyueAQRoUgIg5WRCk5IT2DbC0qT4cKdJ8eOOEODHnhrrx54a68OeGuvDnRqbnnuhhIC/PP9neeUfr8imm2wmDz3yB4SA9HnFM4AATUoApJxMaFJyAosQ4AM2YFROBARAysEEISAHqIEhqbGBoHIwo8bmADUgJEJAAKScTKix6cAWRghYsGCBnHP+WXLaKWfItH+9WlUI0D/LUU8ffRRi9uzZ0rdv33q6rJq4FuXetWtXXt7lkK2Ghgbp37+/w8gde0jdFDU2NkqvXr06NgiHq585c6b07t1bunTp4jB6xx2SGuuXe2qsH3tqrA97aqwPdx21Xmtsv379coVaGCHghRf/JpNuv1n6rNBHZs+ZLQ0N/5GDDvipfPMbWywFQG9u9fShSfHL5pw5c5KmnLd4t38OVNDL++bW/ldV/BFpUvxypN8UqQCDENC+OaDGti/vytGosX7sqbHV2T9y3QJ5bnKjX3IyjLz6ep1l/3OWzxChfl3rtcbm/aVZYYSAyqU59ZWXeDSgfn9XC3NlHFv0SwXHFn3Yc2zRh7uOyrFFH/Y8GuDDXUelxvqxp8ZWZz/58pnyxB2f+iUnw8hrbdRdjpzAn/9tCSE1Nt3CQghIx83MiybFDGV0IJqUaGRmDjQpZiijAiEEROEyNaZJMcUZHIwaG4zK3JAaa440OCA1FiEgeLHUiSE1Nl0iCykEpLuU2vSiSfHLG02KH3uaFB/2CAE+3HVUmhQf9tRYH+46KjXWjz01FiHAb/X5jEyNTccdISAdNzMvmhQzlNGBaFKikZk50KSYoYwKhBAQhcvUmCbFFGdwMGpsMCpzQ2qsOdLggNRYhIDgxVInhtTYdIlECEjHzcyLJsUMZXQgmpRoZGYONClmKKMCIQRE4TI1pkkxxRkcjBobjMrckBprjjQ4IDUWISB4sdSJITU2XSIRAtJxM/OiSTFDGR2IJiUamZkDTYoZyqhACAFRuEyNaVJMcQYHo8YGozI3pMaaIw0OSI1FCAheLHViSI1Nl0iEgHTczLxoUsxQRgeiSYlGZuZAk2KGMioQQkAULlNjmhRTnMHBqLHBqMwNqbHmSIMDUmMRAoIXS50YUmPTJRIhIB03My+aFDOU0YFoUqKRmTnQpJihjAqEEBCFy9SYJsUUZ3AwamwwKnNDaqw50uCA1FiEgODFUieG1Nh0iUQISMfNzIsmxQxldCCalGhkZg40KWYoowIhBEThMjWmSTHFGRyMGhuMytyQGmuONDggNRYhIHix1IkhNTZdIhEC0nEz86JJMUMZHYgmJRqZmQNNihnKqEAIAVG4TI1pUkxxBgejxgajMjekxpojDQ5IjUUICF4sdWJIjU2XSISAdNzMvGhSzFBGB6JJiUZm5kCTYoYyKhBCQBQuU2OaFFOcwcGoscGozA2pseZIgwNSYxECghdLnRhSY9MlEiEgHTczL5oUM5TRgWhSopGZOdCkmKGMCoQQEIXL1JgmxRRncDBqbDAqc0NqrDnS4IDUWISA4MVSJ4bU2HSJRAhIx83MiybFDGV0IJqUaGRmDjQpZiijAiEEROEyNaZJMcUZHIwaG4zK3JAaa440OCA1FiEgeLHUiSE1Nl0iEQLScTPzokkxQxkdiCYlGpmZA02KGcqoQAgBUbhMjWlSTHEGB6PGBqMyN6TGmiMNDkiNRQgIXix1YkiNTZdIhIB03My8aFLMUEYHokmJRmbmQJNihjIqEEJAFC5TY5oUU5zBwaixwajMDamx5kiDA1JjEQKCF0udGFJj0yUSISAdNzMvmhQzlNGBaFKikZk50KSYoYwKhBAQhcvUmCbFFGdwMGpsMCpzQ2qsOdLggNRYhIDgxVInhtTYdIlECEjHzcyLJsUMZXQgmpRoZGYONClmKKMCIQRE4TI1pkkxxRkcjBobjMrckBprjjQ4IDUWISB4sdSJITU2XSIRAtJxM/OiSTFDGR2IJiUamZkDTYoZyqhACAFRuEyNaVJMcQYHo8YGozI3pMaaIw0OSI1FCAheLHViSI1Nl0iEgHTczLxoUsxQRgeiSYlGZuZAk2KGMioQQkAULlNjmhRTnMHBqLHBqMwNqbHmSIMDUmMRAoIXS50YUmPTJRIhIB03My+aFDOU0YFoUqKRmTnQpJihjAqEEBCFy9SYJsUUZ3AwamwwKnNDaqw50uCA1FiEgODFUieG1Nh0iUQISMfNzIsmxQxldCCalGhkZg40KWYoowIhBEThMjWmSTHFGRyMGhuMytyQGmuONDggNRYhIHix1IkhNTZdIhEC0nEz86JJMUMZHYgmJRqZmQNNihnKqEAIAVG4TI1pUkxxBgejxgajMjekxpojDQ5IjUUICF4sdWJIjU2XSISAdNzMvGhSzFBGB6JJiUZm5kCTYoYyKhBCQBQuU2OaFFOcwcGoscGozA2pseZIgwNSYxECghdLnRhSY9MlEiEgHTczL5oUM5TRgWhSopGZOdCkmKGMCoQQEIXL1JgmxRRncDBqbDAqc0NqrDnS4IDUWISA4MVSJ4bU2HSJRAhIx83MiybFDGV0IJqUaGRmDjQpZiijAiEEROEyNaZJMcUZHIwaG4zK3JAaa440OCA1FiEgeLHUiSE1Nl0iEQLScTPzokkxQxkdiCYlGpmZA02KGcqoQAgBUbhMjWlSTHEGB6PGBqMyN6TGmiMNDkiNRQgIXix1YkiNTZdIhIB03My8aFLMUEYHokmJRmbmQJNihjIqEEJAFC5TY5oUU5zBwaixwajMDamx5kiDA1JjEQKCF0udGFJj0yUSISAdNzMvmhQzlNGBaFKikZk50KSYoYwKhBAQhcvUmCbFFGdwMGpsMCpzQ2qsOdLggNRYhIDgxVInhtTYdIlECEjHzcyLJsUMZXQgmpRoZGYONClmKKMCIQRE4TI1pkkxxRkcjBobjMrckBprjjQ4IDUWISB4sdSJITU2XSIRAtJxM/OiSTFDGR2IJiUamZkDTYoZyqhACAFRuEyNaVJMcQYHo8YGozI3pMaaIw0OSI1FCAheLHViSI1Nl0iEgHTczLxoUsxQRgeiSYlGZuZAk2KGMioQQkAULlNjmhRTnMHBqLHBqMwNqbHmSIMDUmMRAoIXS50YUmPTJRIhIB03My+aFDOU0YFoUqKRmTnQpJihjAqEEBCFy9SYJsUUZ3AwamwwKnNDaqw50uCA1FiEgODFUieG1Nh0iUQISMfNzIsmxQxldCCalGhkZg40KWYoowIhBEThMjWmSTHFGRyMGhuMytyQGmuONDggNRYhIHix1IkhNTZdIhEC0nEz86JJMUMZHYgmJRqZmQNNihnKqEAIAVG4TI1pUkxxBgejxgajMjekxpojDQ5IjUUICF4sdWJIjU2XSISAdNzMvGhSzFBGB6JJiUZm5kCTYoYyKhBCQBQuU2OaFFOcwcGoscGozA2pseZIgwNSYxECghdLnRhSY9MlsjBCwJIlS2TMxefLrE9myaLGRjn4wENl3XW+lO6qasiLJsUvWTQpfuxpUnzYIwT4cNdRaVJ82FNjfbjrqNRYP/bUWIQAv9XnMzI1Nh33wggBOv25c+dKz549ZeorL8mdv75dzjx1VLqrqiEvmhS/ZNGk+LGnSfFhjxDgwx0hwI87NdaPPTXWjz01FiHAb/X5jIwQkI57oYSA8iX88Zmn5Lm//kmGHXNCuquqIS+aFL9k0aT4sadJ8WGPEODDHSHAjzs11o89NdaPPTUWIcBv9fmMjBCQjnvhhIALxp4rU19+SUb84nT50jpfXuaqZsyYke5KC+q1aNEimT17tvTt27egM6zfaSn3rl27Svfu3ev3Igt6ZQ0NDdK/f/+Czq5+p6WbosbSo1e9evWq34ss6JXNnDlTevfuLV26dCnoDOtzWtRYv7xSY/3YU2Ors3/42vny3ORGv+RkGHnQkOVk/3OWzxChfl3rtcb269cv16QVTgjQq33/g/fkjFGnyaXjrpRu3botBUBvbvX0oUnxy+acOXOSphwhoP1zoIJe3je39r+q4o+IEOCXIz2NoQIMQkD75oAa2768K0ejxvqxp8ZWZ//IdQtqVghYfb3OCAFVUluvNTbvL80KJQQsXrJYOnfqnLwrYOgJR8mEsVck7wyo5w/HFv2yy7FFP/YcW/Rhz6MBPtx1VI4t+rCnxvpw11GpsX7sqbHV2U++fKY8ccenfsnJMPJaG3WXIycMyBChfl2pselyWxghYOasmXL+RaNLQkAnWbR4sey0w/fk21tvm+6qasiLJsUvWTQpfuxpUnzYIwT4cEcI8ONOjfVjT431Y0+NRQjwW30+IyMEpONeGCEg3fRr34smxS+HNCl+7GlSfNgjBPhwRwjw406N9WNPjfVjT41FCPBbfT4jIwSk444QkI6bmRdNihnK6EA0KdHIzBxoUsxQRgVCCIjCZWpMk2KKMzgYNTYYlbkhNdYcaXBAaixCQPBiqRNDamy6RCIEpONm5kWTYoYyOhBNSjQyMweaFDOUUYEQAqJwmRrTpJjiDA5GjQ1GZW5IjTVHGhyQGosQELxY6sSQGpsukQgB6biZedGkmKGMDkSTEo3MzIEmxQxlVCCEgChcpsY0KaY4g4NRY4NRmRtSY82RBgekxiIEBC+WOjGkxqZLJEJAOm5mXjQpZiijA9GkRCMzc6BJMUMZFQghIAqXqTFNiinO4GDU2GBU5obUWHOkwQGpsQgBwYulTgypsekSiRCQjpuZF02KGcroQDQp0cjMHGhSzFBGBUIIiMJlakyTYoozOBg1NhiVuSE11hxpcEBqLEJA8GKpE0NqbLpEIgSk42bmRZNihjI6EE1KNDIzB5oUM5RRgRAConCZGtOkmOIMDkaNDUZlbkiNNUcaHJAaixAQvFjqxJAam6AI0KsAACAASURBVC6RCAHpuJl50aSYoYwORJMSjczMgSbFDGVUIISAKFymxjQppjiDg1Fjg1GZG1JjzZEGB6TGIgQEL5Y6MaTGpkskQkA6bmZeNClmKKMD0aREIzNzoEkxQxkVCCEgCpepMU2KKc7gYNTYYFTmhtRYc6TBAamxCAHBi6VODKmx6RKJEJCOm5kXTYoZyuhANCnRyMwcaFLMUEYFQgiIwmVqTJNiijM4GDU2GJW5ITXWHGlwQGosQkDwYqkTQ2psukQiBKTjZuZFk2KGMjoQTUo0MjMHmhQzlFGBEAKicJka06SY4gwORo0NRmVuSI01RxockBqLEBC8WOrEkBqbLpEIAem4mXnRpJihjA5EkxKNzMyBJsUMZVQghIAoXKbGNCmmOIODUWODUZkbUmPNkQYHpMYiBAQvljoxpMamSyRCQDpuZl40KWYoowPRpEQjM3OgSTFDGRUIISAKl6kxTYopzuBg1NhgVOaG1FhzpMEBqbEIAcGLpU4MqbHpEokQkI6bmRdNihnK6EA0KdHIzBxoUsxQRgVCCIjCZWpMk2KKMzgYNTYYlbkhNdYcaXBAaixCQPBiqRNDamy6RCIEpONm5kWTYoYyOhBNSjQyMweaFDOUUYEQAqJwmRrTpJjiDA5GjQ1GZW5IjTVHGhyQGosQELxY6sSQGpsukQgB6biZedGkmKGMDkSTEo3MzIEmxQxlVCCEgChcpsY0KaY4g4NRY4NRmRtSY82RBgekxiIEBC+WOjGkxqZLJEJAOm5mXjQpZiijA9GkRCMzc6BJMUMZFQghIAqXqTFNiinO4GDU2GBU5obUWHOkwQGpsQgBwYulTgypsekSiRCQjpuZF02KGcroQDQp0cjMHGhSzFBGBUIIiMJlakyTYoozOBg1NhiVuSE11hxpcEBqLEJA8GKpE0NqbLpEIgSk42bmRZNihjI6EE1KNDIzB5oUM5RRgRAConCZGtOkmOIMDkaNDUZlbkiNNUcaHJAaixAQvFjqxJAamy6RCAHpuJl50aSYoYwORJMSjczMgSbFDGVUIISAKFymxjQppjiDg1Fjg1GZG1JjzZEGB6TGIgQEL5Y6MaTGpkskQkA6bmZeNClmKKMD0aREIzNzoEkxQxkVCCEgCpepMU2KKc7gYNTYYFTmhtRYc6TBAamxCAHBi6VODKmx6RKJEJCOm5kXTYoZyuhANCnRyMwcaFLMUEYFQgiIwmVqTJNiijM4GDU2GJW5ITXWHGlwQGosQkDwYqkTQ2psukQiBKTjZuZFk2KGMjoQTUo0MjMHmhQzlFGBEAKicJka06SY4gwORo0NRmVuSI01RxockBqLEBC8WOrEkBqbLpEIAem4mXnRpJihjA5EkxKNzMyBJsUMZVQghIAoXKbGNCmmOIODUWODUZkbUmPNkQYHpMYiBAQvljoxpMamSyRCQDpuZl40KWYoowPRpEQjM3OgSTFDGRUIISAKl6kxTYopzuBg1NhgVOaG1FhzpMEBqbEIAcGLpU4MqbHpEokQkI6bmRdNihnK6EA0KdHIzBxoUsxQRgVCCIjCZWpMk2KKMyjY2WefLffcc4989tlnst9++8npp58e5IdRNgL//Oc/5YgjjpBPPvlElltuOZk0aZKsvfba2YLiHUTgN7/5jYwePVrmzZsngwcPlltvvVW6d+8e5NtRjCZfPlOeuOPTmrzctTbqLkdOGFCTc89r0uX7zcyZM5O1fvPNN3O/iYCNEBABKw9ThIA8qIbFRAgI45SHFUJAHlTbjokQ0DajvCwQAvIiWz3uCy+8IOuuu6689dZbsuWWW4r+/1VXXbX9J9LBRtT7zIwZM6Rnz55y3XXXyZtvvikTJ07sYBR8Lldr62qrrSbvvvuunHjiibLvvvvKPvvs4zOZgo6KEFDQxKScVvl+o6Lj7bffLq+99hr3mwiWCAERsPIwRQjIg2pYTISAME55WCEE5EG17ZgIAW0zyssCISAvsq3H1Rr7+uuvy7bbbiuvvvqqrLDCCj4T6YCjao2dMGGCdO7cWUaNGtUBCfhdstbYww8/XE499VTZbrvt/CZSwJERAgqYFIMpaY295pprZMmSJdxvIngiBETAysMUISAPqmExEQLCOOVhhRCQB9W2YyIEtM0oLwuEgLzIth73008/lV133VV23313OfbYY30m0UFHveuuu5LHMZ5++mlZaaWVOigFn8sePny46D3n7rvv9plAgUdFCChwcjJMTR+D0cfBuN/EQUQIiONlbo0QYI40OCBCQDAqc0OEAHOkQQERAoIw5WKEEJAL1laDLl68WHbbbTdZc801Zfz48e0/gQ484lNPPSWHHHKIqBiw4YYbdmAS7X/p48aNS96N8dBDD8nyyy/f/hMo+IgIAQVPUIrp6f3moIMOkgceeCB5HIxPOAGEgHBWuVgiBOSCNSgoQkAQplyMEAJywdpmUISANhHlZoAQkBvaqoF186/vBTjzzDNl0KBB7T+BDjqivqhuo402St4PsMEGG0ifPn06KIn2v+wpU6bIwQcfLDfeeCMCTBX8CAHtvy7zHLF8v7nqqqtk88035+WYkbARAiKBWZsjBFgTDY+HEBDOytoSIcCaaFg8hIAwTnlYIQTkQbX1mFtttZXMnj07eWmdfjM6bNgw+f73v9/+E+lgIz766KOyxx57yPrrr5+8H2DAgAFyxx13dDAKPpc7cuRIueWWW2TgwIHJuv/2t7+dvCeAz/8ngBBQX6uhfL8ZMmSIdO3aVVZeeWXuNxEpRgiIgJWHKUJAHlTDYiIEhHHKwwohIA+qbcdECGibUV4WCAF5kW09LjXWh7uOSo31Y0+Nrc4eIcBvXeY5MjU2Hd3CCAFarK+46lL58KMPZeHChbLvXvvLZpt8Jd1V1ZAXTYpfsmhS/NjTpPiwRwjw4a6j0qT4sKfG+nBHCPDjriNTYxECfFdg+49OjU3HvDBCgE7/zX+/If+zxpryxpuvy/jLx8klYy5Ld1U15EWT4pcshAA/9jQpPuwRAny4IwT4cafG+rGnxvqxp8YiBPitPp+REQLScS+UEFC+hJf/MVVuu2OSnD1ydLqrqiEvmhS/ZNGk+LGnSfFhjxDgwx0hwI87NdaPPTXWjz01FiHAb/X5jIwQkI574YSAhhkNMuq8M+WYI4bK2l8cvMxVzZgxI92VFtRr0aJFycuM+vbtW9AZ1u+0lLu+WKR79+71e5EFvbKGhgbp379/QWdXv9PSTVFjY6P06tWrfi+yoFc2c+ZM6d27t3Tp0qWgM6zPaVFj/fJKjfVjT42tzv7ha+fLc5Mb/ZKTYeRBQ5aT/c/hT0K2hLBea2y/fv0yrJi2XQslBMz6ZJacNXqkHLDfT2STjTZtcfZ6c6unD02KXzbnzJmTNOUIAe2fAxX08r65tf9VFX9EhAC/HOlpDBVgEAKWzcHUJxpl8iUL/JKTYeSupZ78hEk9M0Twdb3ltM/krZcX+04i5ehf3aWLbHdwt5Tevm4N7yyWq4/9zHcSGUb/2aXLS//Pd84Qwc/1kesW1KwQsPp6nWtaCBi731xZWKPLfpdh3WTIVu0v5Of9pVmhhIAxF58vX//a5rLVFlv73SHaeWSOLbYz8IrhOLbox55jiz7seTTAh7uOyrHF6uynPDxXJo2qTZG/W49OMvr3X/BbWBlHvnLoh/L6C/MzRvFx32qvFWSXo1f0GTzjqB+91SgX/vi9jFH83E+6+XOyyurtvymyuGL+aoAFxXQxTt3pbVkwb0k6Z2ev/U7vL5tsX7uibzV8hREC9Ljqjw/ZR9b78pCmuR512LGycv+VnVOf7/AIAfnybS06QoAfe4QAH/YIAT7cEQJa544Q4LcuEQJ82CME+HDXUREC/NgjBPixL7wQUDw07TMjhID24dzSKAgBfuwRAnzYIwT4cEcIQAjwW3mtj4wQ4JMZhAAf7ggBftx1ZIQAX/4tjV6YEwHFQ9M+M0IIaB/OCAF+nFsaGSHAJx8IAT7cEQIQAvxWHkJAEdkjBPhlhRMBfuwRAvzYVxsZIcA5JwgBfgngRIAfe4QAH/YIAT7cEQIQAvxWHkJAEdkjBPhlBSHAjz1CgB97hIDisU9mhBDglxiEAD/2CAE+7BECfLgjBCAE+K08hIAiskcI8MsKQoAfe4QAP/YIAcVjjxDgnBOEAL8EIAT4sEcI8OGOEIAQ4LfyEAKKyB4hwC8rCAF+7BEC/NgjBBSPPUKAc04QAvwSgBDgwx4hwIc7QgBCgN/KQwgoInuEAL+sIAT4sUcI8GOPEFA89ggBzjlBCPBLAEKAD3uEAB/uCAEIAX4rDyGgiOwRAvyyghDgxx4hwI89QkDx2CMEOOcEIcAvAQgBPuwRAny4IwQgBPitPISAIrJHCPDLCkKAH3uEAD/2CAHFY48Q4JwThAC/BCAE+LBHCPDhjhCAEOC38hACisgeIcAvKwgBfuwRAvzYIwQUjz1CgHNOEAL8EoAQ4MMeIcCHO0IAQoDfykMIKCJ7hAC/rCAE+LFHCPBjjxBQPPYIAc45QQjwSwBCgA97hAAf7ggBCAF+Kw8hoIjsEQL8soIQ4MceIcCPPUJA8dgjBDjnBCHALwEIAT7sEQJ8uCMEIAT4rTyEgCKyRwjwywpCgB97hAA/9ggBxWOPEOCcE4QAvwQgBPiwRwjw4Y4QgBDgt/IQAorIHiHALysIAX7sEQL82CMEFI89QoBzThAC/BKAEODDHiHAhztCAEKA38pDCCgie4QAv6wgBPixRwjwY48QUDz2CAHOOUEI8EsAQoAPe4QAH+4IAQgBfisPIaCI7BEC/LKCEODHHiHAjz1CQPHYIwQ45wQhwC8BCAE+7BECfLgjBCAE+K08hIAiskcI8MsKQoAfe4QAP/YIAcVjjxDgnBOEAL8EIAT4sEcI8OGOEIAQ4LfyEAKKyB4hwC8rCAF+7BEC/NgjBBSPPUKAc04QAvwSgBDgwx4hwIc7QgBCgN/KQwgoInuEAL+sIAT4sUcI8GOPEFA89ggBzjlBCPBLAEKAD3uEAB/uCAEIAX4rDyGgiOwRAvyyghDgxx4hwI89QkDx2CMEOOcEIcAvAQgBPuwRAny4IwQgBPitPISAIrJHCPDLCkKAH3uEAD/2CAHFY48Q4JwThAC/BCAE+LBHCPDhjhCAEOC38hACisgeIcAvKwgBfuwRAvzYIwQUjz1CgHNOEAL8EoAQ4MMeIcCHO0IAQoDfykMIKCJ7hAC/rCAE+LFHCPBjjxBQPPYIAc45QQjwSwBCgA97hAAf7ggBCAF+Kw8hoIjsEQL8soIQ4MceIcCPPUJA8dgjBDjnBCHALwEIAT7sEQJ8uCMEIAT4rTyEgCKyRwjwywpCgB97hAA/9ggBxWOPEOCcE4QAvwQgBPiwRwjw4Y4QgBDgt/IQAorIHiHALysIAX7sEQL82CMEFI99MqMpU6bILrvsIvfff79svPHGBZ1l/U1r8eLFctxxx8mbb74pkydPrr8LLPAVzZw5U3beeWfZc889ZdiwYQWeaf1NDSHAJ6d6vzn66KPlnXfekfvuu89nEgUedcrDc2XSqIYCz7D61Lr16CSjf/+Fmpy7TvrKoR/K6y/Mr8n5b7XXCrLL0SvW5NwRAvzShhDgxx4hwI89QkDx2Mudd94pY8aMkRkzZshdd92FENBOOVqyZIlsueWWsuKK/20gHnjggXYamWFee+012XXXXWWFFVaQvffeGyGgnZcEQkA7Ay8NV77f6Jrv2rUrwmMLKUAIaP91WR4RIcCHPUKAD3cdFSHAjz1CgB97hIDisZd58+ZJp06dZPvtt5fLLrsMIaAdczR37lx55JFHZOLEiQgB7chdvxlduHChnHzyybLGGmsgBLQjex0KIaCdgf/fcHq/+d3vfifXX389QgBCgM8irDIqQoBPOhACfLgjBPhx15ERAnz5tzR6p9K3FUuKN62OM6P58+fLdttthxDgkHJ9JAAhwAF8acjhw4cjBDigRwhwgP5/Q959990IAVXwcyLAb10iBPiwRwjw4Y4Q4McdIcCXfbXREQKc84IQ4JcAhAA/9ggBPuwRAny466gIAdXZIwT4rUuEAB/2CAE+3BEC/LgjBPiyRwgoJn9BCPBLDEKAH3uEAB/2CAE+3BECWueOEOC3LhECfNgjBPhwRwjw444Q4MseIaCY/BECHPOCEOAHHyHAhz1CgA93hACEAL+V1/rICAE+mUEI8OGOEODHHSHAlz1CQDH5J0KA/j37QYMGFXSG9Tst5d6jRw/p06dP/V5kQa9s2rRpMnjw4ILOrn6nhRDgl9vp06fLwIEDpXv37n6TKOjInAjwSwxCgA97hAAf7ggBftwRAnzZ14wQ8Oyfn5Zrb7hGrrr82mISM54VQoAx0IhwCAERsIxNEQKMgQaGQwgIBJWDGUJAdagIATksuMCQCAGBoIzNEAKMgUaE488HRsAyNuWvBhgDNQhXqJcF3nDzddIwo0Femvp3ufaXNxpcXvFDIAT45QghwI89QoAPe4QAH+46KkIAQoDf6qs+MkKAT1YQAny466gIAX7sEQL82FcbuVBCgG6K9djkIUccKNdORAgo3nKprxkhBPjlEyHAhz1CgA93hIDWuXMiwG9dIgT4sEcI8OGOEODHXUdGCPDl39LohRICyhNsTQiYMWNG8ShmmNGiRYtk9uzZ0rdv3wxRcE1DQLl37dqVZ3bTwMvo09DQIP37988YBfdYAiq2NjY2Sq9evWJdsc9IYObMmdK7d2/p0qVLxkj15z71iUa57+L5NXlhXZcXOfHW2v19uuW0z2T61EU1yf6ru3SR7Q+pzXduNLyzWK46Zl5NctdJH3ZZD+n/+c41Of+Hr50vz01urMm5DxqynOx/TummU6Ofi/adIws/q83J/2B4dxmyVfvX7379+uUKrOaEAN1AtPdHm5TJlyxo72FNxtMm5YRJPU1ieQTRJuWtlxd7DJ15TG1Stju4W+Y4HgG0Sbn62Bq9W5eA/ezS5Wu2Sckz3wgBrdMdu9/cmm1SdhnWzaVJsViv1FgLiuliUGPTccvqRY3NSjC9/yPXLahZIWD19TrXtBBAjY1ft3l/aVZzQkA8wuweHFvMzjBtBI4tpiWXzY9ji9n4FdWbRwNazwzHFn1WLjXWh7uOSo31YU+N9eGuo/KOAD/21Fg/9tVGRggIyAlNSgCknExoUnIC20ZYmhQf7nmPihCAEJD3GksTnxqbhpqNDzXWhmNsFGpsLDE7e4QAO5axkRACYonlb19IISD/y44bgSYljpelNU2KJc3wWDQp4axqyRIhACGgiOuVGuuXFWqsD3tqrA93HRUhwI89QoAf+2ojIwQE5IQmJQBSTiY0KTmBbSMsTYoP97xHRQhACMh7jaWJT41NQ83GhxprwzE2CjU2lpidPUKAHcvYSAgBscTyt0cICGBMkxIAKScTmpScwCIE+IB1HhUhACHAeQm2ODw11i8r1Fgf9ggBPtx1VIQAP/YIAX7sq42MEBCQE5qUAEg5mdCk5AQWIcAHrPOoCAEIAc5LECGgYAmgxvokBCHAhztCgB93HRkhwJd/S6MjBATkBCEgAFJOJjQpOYFFCPAB6zwqQgBCgPMSRAgoWAKosT4JQQjw4Y4Q4McdIcCXfbXREQIC8oIQEAApJxOalJzAIgT4gHUeFSEAIcB5CSIEFCwB1FifhCAE+HBHCPDjjhDgyx4hIAN/hIAM8DK60qRkBJjSnSYlJbiCuyEEIAQUcYlSY/2yQo31YU+N9eGOEODHHSHAlz1CQAb+NCkZ4GV0pUnJCDClO01KSnAFd0MIQAgo4hKlxvplhRrrw54a68MdIcCPO0KAL3uEgAz8aVIywMvoSpOSEWBKd5qUlOAK7oYQgBBQxCVKjfXLCjXWhz011oc7QoAfd4QAX/YIARn406RkgJfRlSYlI8CU7jQpKcEV3A0hACGgiEuUGuuXFWqsD3tqrA93hAA/7ggBvuwRAjLwp0nJAC+jK01KRoAp3WlSUoIruBtCAEJAEZcoNdYvK9RYH/bUWB/uCAF+3BECfNkjBGTgT5OSAV5GV5qUjABTutOkpARXcDeEAISAIi5RaqxfVqixPuypsT7cEQL8uCME+LJHCMjAnyYlA7yMrjQpGQGmdKdJSQmu4G4IAQgBRVyi1Fi/rFBjfdhTY324IwT4cUcI8GWPEJCBP01KBngZXWlSMgJM6U6TkhJcwd0QAhACirhEqbF+WaHG+rCnxvpwRwjw444Q4MseISADf5qUDPAyutKkZASY0p0mJSW4grshBCAEFHGJUmP9skKN9WFPjfXhjhDgxx0hwJc9QkAG/jQpGeBldKVJyQgwpTtNSkpwBXdDCEAIKOISpcb6ZYUa68OeGuvDHSHAjztCgC97hIAM/GlSMsDL6EqTkhFgSnealJTgCu6GEIAQUMQlSo31ywo11oc9NdaHO0KAH3eEAF/2CAEZ+NOkZICX0ZUmJSPAlO40KSnBFdwNIQAhoIhLlBrrlxVqrA97aqwPd4QAP+4IAb7sEQIy8KdJyQAvoytNSkaAKd1pUlKCK7gbQgBCQBGXKDXWLyvUWB/21Fgf7ggBftwRAnzZIwRk4E+TkgFeRlealIwAU7rTpKQEV3A3hACEgCIuUWqsX1aosT7sqbE+3BEC/LgjBPiyRwjIwJ8mJQO8jK40KRkBpnSnSUkJruBuCAEIAUVcotRYv6xQY33YU2N9uCME+HFHCPBljxCQgT9NSgZ4GV1pUjICTOlOk5ISXMHdEAIQAoq4RKmxflmhxvqwp8b6cEcI8OOOEODLHiEgA3+alAzwMrrSpGQEmNKdJiUluIK7IQQgBBRxiVJj/bJCjfVhT4314Y4Q4McdIcCXPUJABv40KRngZXSlSckIMKU7TUpKcAV3QwhACCjiEqXG+mWFGuvDnhrrwx0hwI87QoAve4SADPxpUjLAy+hKk5IRYEp3mpSU4AruhhCAEFDEJUqN9csKNdaHPTXWhztCgB93hABf9ggBGfjTpGSAl9GVJiUjwJTuNCkpwRXY7fHHH5dhw4bJokWLZMSIEbLPPvsUeLY+Uzt1p7dlwbwlPoNnHHW/0/vLJtv3zBjFx50a68NdR6XG+rCnxvpwRwjw444Q4MseISADf5qUDPAyutKkZASY0p0mJSW4grotXrxY1l13XbnnnnukR48essMOO8iUKVOkb9++BZ2xz7QQAny4U2N9uCME+HGnxvqxn3z5THnijk/9JpBh5LU26i5HThiQIYKvKzXWl39Lo3daUvoUb1rFmhFNil8+EAJ82NOk+HDPa9Rp06bJXnvtJY899pjMnz9fjjrqKDn00ENlp512ymvImoxLk+KTNmqsD3eEAD/u1Fg/9ggBfuypsX7sq42MEBCQE5qUAEg5mSAE5AS2jbA0KT7c8xr1mWeeSR4HuPfeexMhQP/31ltvLQcccEBeQ9ZkXJoUn7RRY324IwT4cafG+rFHCPBjT431Y48QkIE9TUoGeBldEQIyAkzpTpOSElxB3V599VXZe++9ORHQRn5oUnwWMDXWhztCgB93aqwfe4QAP/bUWD/2CAEZ2NOkZICX0RUhICPAlO40KSnBFdRN3xGwzjrrJCcCyu8IeP7552XFFVcs6Ix9pkWT4sOdGuvDHSHAjzs11o89QoAfe2qsH3uEgAzsaVIywMvoihCQEWBKd5qUlOAK7KbvBxg+fDh/NaCVHNGk+CxgaqwPd4QAP+7UWD/2CAF+7KmxfuwRAjKwp0nJAC+jK0JARoAp3WlSUoIruNusWbOSdwQMGFC7bx3OEzFNSp50q8emxvpwRwjw406N9WOPEODHnhrrx74mhICXX5kqN95yneifMfjBzrvJFptvWQhiNCl+aUAI8GFPk+LDPe9REQJaJ0yTkvcKbDk+NdaHO0KAH3dqrB97hAA/9tRYP/aFFwIWL1ksw048Rk4/5Szp1auXnDTieLlw9Djp2bOnOzWaFL8UIAT4sKdJ8eGe96gIAQgBea+xNPGpsWmo2fhQY204xkahxsYSs7NHCLBjGRsJISCWWP72hfnzge+9/55cPGGMXHjuuOSqx46/ULbbZgfZeKNN8qfQxgg0KX4poEnxYU+T4sM971ERAhAC8l5jaeJTY9NQs/GhxtpwjI1CjY0lZmePEGDHMjYSQkAssfztCyMEvDrtnzLpjpvlzFNHJVc98erLZb0vD5GtttxmKQozZszIn0qzEaY+0Sj3XTy/3ce1GLDr8iIn3trLIpRLjFtO+0ymT13kMnbWQb+6SxfZ/pDuWcO4+De8s1iuOmaey9gWgx52WQ/p//nOFqHqKoa+H6CxsTE5dcVnWQIX7TtHFn5Wm2R+MLy7DNmqS01OnhrrlzZqrA97aqwPdx314Wvny3OTG/0mkGHkQUOWk/3PKTX2NfqhxsYnrl+/fvFOER6FEQLefe9dueTSi9o8EdDQ0BBxecU3XbRokcyePVv69u1b/MnW2QznzJkjXbp0ke7da3OzXsvpUEEv75tbLfPJa+4IAXmRbTuunsZQAUbvOXzajwA1tv1YNx+JGuvHnhrrw54a68NdR63XGtu/f/9coRZGCNB3BBx3wtEycsTZTe8IuGD0WOnVs76/udKbxgcffCCDBg3KNdEEX5aActe/qd6nTx/wtDOBadOmyeDBg9t5VIbj0QC/NTB9+nQZOHAgwmM7p4Aa287AK4ajxvqxp8b6sKfG+nDXUamx6dgXRgjQ6U995SW56ZbrZfGSJbLr93cvzF8NSIc2zIsmJYxTHlY0KXlQDYtJkxLGydqKJsWaaHg8mpRwVpaW1FhLmnGxqLFxvCytqbGWNMNjUWPDWVlbUmPTES2UEJDuEmrbiybFL380KX7saVJ82NOk+HDXUWlSfNhTY32466jUWD/21Fgf9tRYH+7U2PTcEQLSszPxpEkxwZgqCE1KKmwmTjQpJhijg9CkRCMzc0AIMEMZFYgaegoTrgAAD+9JREFUG4XL1Jgaa4ozKhg1NgqXmTE11gxldCBqbDSyxAEhIB03My+aFDOU0YFoUqKRmTnQpJihjApEkxKFy9SYJsUUZ3AwamwwKnNDaqw50uCA1NhgVKaG1FhTnFHBqLFRuJqMEQLSccMLAhCAAAQgAAEIQAACEIAABCBQkwQQAmoybUwaAhCAAAQgAAEIQAACEIAABCCQjgBCQDpueEEAAhCAAAQgAAEIQAACEIAABGqSAEJATaaNSUMAAhCAAAQgAAEIQAACEIAABNIRQAhIxw0vCEAAAhCAAAQgAAEIQAACEIBATRJACKjJtDFpCEAAAhCAAAQgAAEIQAACEIBAOgIIAem44QUBCEAAAhCAAAQgAAEIQAACEKhJAggBOaTt/t9Nlqf++Lgs16WLHP7To2TQ6oOSUebOnSu33XmL/Ov1f8keu+8lm268WdPoN992o6y26mqy7Tbby7N/flquveEauerya5OfP/fXP8uvf3OXLFm8WFZeeRU57ujjpWvXrjnMvPZDzpk7R8aNHyObbrKZ7LzTLrJkyRIZc/H5MuuTWbKosVEOPvBQWXedLzVdqNqPGHmSXDzmUpk3b95Svmp01z13JPwXLWqUb3ztm0ne+CxNQP9O9xVXXSoffvShLFy4UPbda3/ZbJOvJEbVfhf0Z5Vr/q23p8t5Y0bLScefLP+zxpry7nvvyFXXTpQFCxZIp06d5Ngjj5NVB34O9M0IVON00SUXyOzZsxNrtfnODt+VH+22Z5P3iDN+IUOPGiYDBgyUW2+/JbH5+fCTud9ErLAZMxrk0ivHl+4bc2XIehvIAfv9pM37zZQXnpennn5C9t/7gGV8y0M3v/9HTKnDmC5esniZddtYur//8ldXyFtvvyX9VuonxxxxnPTs2XOZGqv3kRtvuU6WlH7yg513ky023zKxaV47OgzMyAttzuk/DR/JZVdOaIoy7bVX5fSTz5QvrfvlJq5aYy8675LSPf3KFvPT/P4fOaW6N2+txurFfzzzYxl59gjZpbSed9zuO8us+Y033KTF+01Lv0d1DzPyAj/6z0cy/rKxiVfXrt1k2LEnSN8+fZP/39q6LdfYvzz/lxb3A9xv2k5ESzW2rftNazW2rd+jtmdUnxYIAcZ5ff+D9+TCcefJmHMvltdKG/6bbr1BRo08VxaXNvGnlIrhdqWN/o7b77TUqPqzYT8/Ri44Z6zccfdt0lBqMF+a+ne59pc3Jnbvvf+erLTSSrJ89+WT2Fts/q2m5sV4+jUd7v0P3k/46E36K5t9NREC9KMCjDaEU195Se789e1y5qmjmq7zwYd/LzM+niHbbLVti75vTn9T1lh9DVnYuFCOOu5wuejccbLiiivVNKc8Jv/mv99INvBvvPm6jL98nFwy5jKp9rug41eu+b/9fYpMfuBemVsSYoYdc3wSR3M2e86nMmCVgYmYoBvVww45Io+p13TMtjjp2j5r9Oly7tkXSq+evZJrfefdt0sbpivlrNPOkdNLzeOKfVcs5WNJIsJwvwlfDpdeeYlsvOGm8q0ttpLRF5xdasJ3lQ3X36jV+83Fl14k22+7ozz2xKMt+t5w83XL3P/DZ9QxLFXcbWndPvrYw/Lqv16VIw49Su65727Rpm+fPfdLoJTvN+eNGiOnnP5zOf2Us6RXr15y0ojj5cLR4+STTz9p8f7fMYiGX2W1GluO8Pzf/iq/+8MDcuovRi5TYwesMqDF/Dzzp6eXuf+Hz6jjWLZUY/XqVXy/YNy50rvXCrLel4c0CQGVNfaa63+5zP1mgyEbtvh71HGIhl2p3m/mfTZPevbomfTn+sXEnj/cW1pbt+Uae9Rhx7S4H2jr9yhsZvVvVa3Gtna/aavGVvs9qn+a1a8QIcA4+w//70Py9jtvyUE/PiSJ/NMjfiJXTrhann3uGXns8Udl5IizlhmxrGAde+SwpHnp3r27HHLEgXLtxP8KAZWfs0aPlB/utofoTZzP0gRU3V7UuEjuK20ql19++SYhoGz1x2eeKn27/6fSZvOEJsfKb0Zb89XTAsedeLSMv+hy6dGjB+irEHj5H1PltjsmydkjR0u134Vu3bpJ5ZrXb/31hMuo88+UA/c7KBECKj96KkO/7Ss39cBvmUBLnK678VfyuVU/Jzvt+L0mp8qTGHq/UfHmvgd+0yQEcL8JW2GHH/NTufjCSxOR8fcP/jbZwO+/zwFNzs3vN7PnzJZTSycx9PTRkcf+rEXftu7/YTOrf6uW1u2EKy6Wzb++hXx1s6/J9Lemy8RrLpdzz7oggVG+3+yx+95y8YQxcmFJ0NXP2PEXlsT5HWTDDTdqtXbUP9GwK2yrxqqwcvQRQ2WNQf+zTI3VTVRL+Wnr/h82s45jVVlj9apVPFx/yAbJF0/6JUX5REBlja12r2rr/t9xqIZdqZ5S1P5EGbe2bss1dnFJRGhpP9ClaxfuNwHI26qxze83ITW2PGzz36OA6dStCUKAcWr1mwjdtKhiqJ9jjz9Szih9A33f/ffISy+/lDTlnTp1lr1+tE/TIwPjJlwkO2y341Kb+5aEgLvvvbPUtL8hJw47yXjW9RVOOTUXAi4Ye65MLfEf8YvT5Uvr/PfI4tvvvJ0cVTz79NFNAFry1XxeePF5sslGm8p3d9y5vmAZXo1uhEadd2bpSO5QWfuLg5Nv5Vr6XVi5/8rS0po/+7wzlhECXiydjLnuxmtKwoJ+49HbcLb1FaolTipenXDycYl4VX6USL8lUkFLvwUtC1r/+OcrLQoB3G9aXyP7HbSXTLr+jsToidKjYFNfflGO/Nkxyf9v6X7zh4d/Jx9//HEiaLXmq/7VhOD6WrXZrqb5uj3ngrPkhz/YI/lWVO9FZ4w6TS67+MpkkPL9pnu37jLpjpubToVNvPryxH6rLbdJ7Fq6/2ebZX16t8RJT9zdd/+9csrPT2u66Moa21p+1KGl+3990kt/Vc1rrH67qSKuPualQkulEFBZY1u731S7/6efZf156jf4etJxTknM1V5ET9GVP83XbWWN/f1Dv63aA3G/aXudtLZuW7rfhNbY5r9Hbc+kvi0QAozz+/CjD8rbpaO3zU8E3Fx6ROCLaw2Wrb+1jbz8ylS5tfSuAH1kQBWsESN/IZdcdKl0LgkE5U/zRvD+394nf3txSulbu1OkW+k5JT7VCVRr5vSoujaHl467UvRbac3Jap/7fPJehvKnua9+AzL2kgtLos0asvce+4K9CgF9B4OeVtHnpFUw0U+134UFCxe0uOabF1RtUCZec4WMOOm05BEBPi0TqMbp6Wefkr+/9PfkqHT5o0d3//jMk6V3Lgxr+reWGkHuN22vtsNKJwIuaeVEQPP7jT4nPfTo4cm7LtryRQhom3/zdauN+je/seUyJwIqa+z7778vl5Qez2h+ImDjjTZJBkQIaJt7NU5XX/fLRGTXR2XKn8oaWy0/ZVuEgNbZt1Rj9RTYCy/+LXlsVO83KviecNxJ0re0Ua3sK1u73yAEhK15tdL3uzz+5GNLPfrSfN1W1thqPZD2n9xv2ube2rpt6X4TUmNb+j1qeyb1bYEQYJxffZ5fX043pnT08LU3XpObJl2fbPj15vHGv19PBIJ/vTZNbii9rEj//fcP/U5mll700vzYc2UjqEd3r7jqMjnnjPOSxwb4tE6gpc28iiz6PPXQE46SCWOvSE4MNP9mtKUb829/f3/pmOm/5YifHQ32Vgjomv/61zaXrbbYusmq2u9CtTVfWVD12N2JpwyXk08ckYg1fFom0BonvWfoS6K++Y0tmpzHlY5F71A61lj5aFHzRpD7Tdhqq/b8ooqHze83Mz5uSF5+WT591NazjwgBbeeg+bqtfEfAvZN/LZ999llSVyvvN5qb4044uvSI3tlN7wi4YPTYpvdnIAS0zb3aBuaY4UfI6NKjGOUXqTU/fVQtPwgBYcxbqrGVnpUnAprX2NbuNwgBrfPXddy583+/pFNW19/0Kzn/nIuanJoLAZU1tloPVHbmftM6+9bWbfP7jT6CEVJj2/o9CvttrC8rhIAc8qkvN3tS/2rAcsuVvo07OnkEoLH01vmrfjWx9GK6Blkwf4H85McHl04IrJ28QPC4//uWqHIqlY3g7XfdWor3ROkN3wMSk/VLb6j+4a575DDz+ghZeXOdOWumnH/R6FJj3kkWlW7oO+3wPfn21ttKS9+MttTgjBx1ailf86Vn6cVS+tEXEJbfiF8ftLJfhR7///Eh+yRHbMufow47VvQRgJZ+F6qt+cqC+tLUF5Pnd9dcc60kpL4I6fihJ2afbJ1FaI3TyaWXomkeyn+1RP+KgL4To/npo+aNIPebsEWixwsva/ZXA6rdb26adIN8frX/f/qoJd9q9/+w2XQ8q+brtvKvBqxUelZaT73o+xua32/0SOlNt1wv+vzurt/ffakX79KYh62j5pxUdDl62OHyq4k3NAVoXmOr5afswImA6uxbq7Flr0ohoPmab+1+gxDQ+prXdy3cfuek5K+A6YsCD9z/YFln7XWanCrXbUs1tqUeqOzM/aZ19tXWbUv3m5AaG/J7FHYHrC8rhADHfOqfHtHjLZXPqDtOp0MNrZtM/esNvHSxfdPOmm9f3pWj6QvtdKPKSxfbNwf6jdLQ0rfQekqMF422L3vuN+3Lu3I0aqwPe9a8D3cdlRrrw54am407QkA2fpm8byw9NvCF1b6w1DPqmQLiHETgv6rtSaVvRi9b6r0MQc4YZSLAms+EL5OznhDQP8+oz6jzaT8Cf53yF9H3NVS+l6H9Ru/YI3G/8ck/NdaHu47KmvdjT431YU+NzcYdISAbP7whAAEIQAACEIAABCAAAQhAAAI1RQAhoKbSxWQhAAEIQAACEIAABCAAAQhAAALZCCAEZOOHNwQgAAEIQAACEIAABCAAAQhAoKYIIATUVLqYLAQgAAEIQAACEIAABCAAAQhAIBsBhIBs/PCGAAQgAAEIQAACEIAABCAAAQjUFAGEgJpKF5OFAAQgAAEIQAACEIAABCAAAQhkI4AQkI0f3hCAAAQgAAEIQAACEIAABCAAgZoigBBQU+lishCAAAQgAAEIQAACEIAABCAAgWwEEAKy8cMbAhCAAAQgAAEIQAACEIAABCBQUwQQAmoqXUwWAhCAAAQgAAEIQAACEIAABCCQjQBCQDZ+eEMAAhCAAAQgAAEIQAACEIAABGqKAEJATaWLyUIAAhCAAAQgAAEIQAACEIAABLIRQAjIxg9vCEAAAhCAAAQgAAEIQAACEIBATRFACKipdDFZCEAAAhCAAAQgAAEIQAACEIBANgIIAdn44Q0BCEAAAhCAAAQgAAEIQAACEKgpAggBNZUuJgsBCEAAAhCAAAQgAAEIQAACEMhGACEgGz+8IQABCEAAAhCAAAQgAAEIQAACNUUAIaCm0sVkIQABCEAAAhCAAAQgAAEIQAAC2Qj8PxGNyT2vkfe5AAAAAElFTkSuQmCC'
+      message.body = message.body.replace '{bdc}', "<img src='#{src}' />"

-      srcBis = if useCid then 'cid:image' else base64bis
-
-      message.body = message.body.replace '{bdc}', "<img src='#{src}' /><img src='#{srcBis}' />"
       if useCid
-        message.cids = [ {
+        message.cids = [{
           type: 'image/png'
           name: 'BDC'
           base64: bdcBase64.split(',')[1]
           id: 'bdc'
-        },
-        {
-          type: 'image/png'
-          name: 'Image'
-          base64: base64bis.split(',')[1]
-          id: 'image'
-        } ]
-      message
+        }]
+      renderImages(message, useCid)
+      return message

   renderTo = (project) ->
     emails = (member.email for member in project.team when member.daily is 'to')


FIX: autocorrect / autocapitalize should work again

--- source/Art/Engine/Forms/TextInputElement.coffee
+++ source/Art/Engine/Forms/TextInputElement.coffee
@@ -48,7 +48,7 @@
   normalizeAuto = (v) ->
     if v?
       v || "off"
-    else "off"
+    else undefined

   logEventErrors = (handlerMap) ->
     object handlerMap, (handler, eventName) ->
@@ -63,7 +63,7 @@

   constructor: (options = {}) ->
     @_focusEventsDisabled = false
-    props = object
+    props = merge
       placeholder:    options.placeholder || ""
       type:           options.type
       # NOTE: moving towards using 100% lowerCamelCase in Art.Engine - even if HTML5's name is full-lower-case


fixed multiline, empty lines

fixed multiline, empty lines

--- lib/adafruit_i2c_lcd.coffee
+++ lib/adafruit_i2c_lcd.coffee
@@ -70,7 +70,7 @@
 				else
 					@emit 'button_down', key
 				@BSTATE = cur
-		pollInterval
+		,pollInterval

 	colors:
 		OFF: 0x00
@@ -108,9 +108,10 @@
 	message: (text) ->
 		lines = text.split('\n')    # Split at newline(s)
 		for line, i in lines
-			if i>0
+			if i==1
 				@writeByte 0xC0
-			@writeByte(line, true)       # Issue substring
+			if i<2
+				@writeByte(line, true)       # Issue substring

 	buttonState: () ->
 		ret= @WIRE.readBytes MCP23017_GPIOA,1
@@ -241,11 +242,12 @@
 				# Append 4 bytes to list representing PORTB over time.
 				# First the high 4 data bits with strobe (enable) set
 				# and unset, then same with low 4 data bits (strobe 1/0).
-				data = data.concat(@maskOut(bitmask, value[k].charCodeAt(0)))
-				if (data.length >=32 || k == last)
-					@sendBytes(MCP23017_GPIOB, data)
-					@PORTB = data[data.length-1]
-					data=[]
+				if value[k]?
+					data = data.concat(@maskOut(bitmask, value[k].charCodeAt(0)))
+					if (data.length >=32 || k == last)
+						@sendBytes(MCP23017_GPIOB, data)
+						@PORTB = data[data.length-1]
+						data=[]
 		else
 			# Single byte
 			data=@maskOut(bitmask, value)


Better way to get the element.

Better way to get the element.

--- src/extractor.coffee
+++ src/extractor.coffee
@@ -9,8 +9,8 @@
     titleText = titleElement.attr("content") if titleElement

     if !titleText
-      titleElement = doc("title")
-      titleText = titleElement.first().text()
+      titleElement = doc("title").first()
+      titleText = titleElement.text()

     return null unless titleElement


file_Delete returns null when there is not file

--- src/fluent-fs.coffee
+++ src/fluent-fs.coffee
@@ -33,7 +33,7 @@

 String::file_Delete         = ->
                                   file = @.toString().realPath()
-                                  return null if not file
+                                  return true if not file
                                   fs.unlinkSync file
                                   return file.file_Not_Exists()

@@ -59,10 +59,7 @@
                                     return (file for file in files when file.path_Extension() is extension)
                                   return files

-String::folders             = ->
-                                  path = @.toString()
-                                  path.path_Combine(item).realPath() for item in fs.readdirSync path  when item.is_Folder()
-
+String::folders             = ->  item for item in @.files_and_Folders() when item.is_Folder()

 String::is_Folder           = ->  try fs.lstatSync(@.toString()).isDirectory() catch then false
 String::is_File             = ->  try fs.lstatSync(@.toString()).isFile()      catch then false


Support ordered list

Support ordered list

--- src/js/esarea.coffee
+++ src/js/esarea.coffee
@@ -58,15 +58,15 @@
   return if e.metaKey || e.ctrlKey || e.shiftKey # for cmd + enter
   return unless currentLine = getCurrentLine(e)
   return if currentLine.start == currentLine.caret
-  if match = currentLine.text.match(/^(\s*(?:-|\+|\*) (?:\[(?:x| )\] )?)\s*\S/)
+  if match = currentLine.text.match(/^(\s*(?:-|\+|\*|1\.) (?:\[(?:x| )\] )?)\s*\S/)
     # smart indent with list
-    if currentLine.text.match(/^(\s*(?:-|\+|\*) (?:\[(?:x| )\] ))\s*$/)
+    if currentLine.text.match(/^(\s*(?:-|\+|\*|1\.) (?:\[(?:x| )\] ))\s*$/)
       # empty task list
       $(e.target).selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)})
       return
     e.preventDefault()
     $(e.target).selection('insert', {text: "\n" + match[1], mode: 'before'});
-  else if currentLine.text.match(/^(\s*(?:-|\+|\*) )/)
+  else if currentLine.text.match(/^(\s*(?:-|\+|\*|1\.) )/)
     # remove list
     $(e.target).selection('setPos', {start: currentLine.start, end: (currentLine.end)})
   else if currentLine.text.match(/^.*\|\s*$/)
@@ -90,7 +90,7 @@
 handleSpaceKey = (e) ->
   return unless e.shiftKey && e.altKey
   return unless currentLine = getCurrentLine(e)
-  if match = currentLine.text.match(/^(\s*)(-|\+|\*) (?:\[(x| )\] )(.*)/)
+  if match = currentLine.text.match(/^(\s*)(-|\+|\*|1\.) (?:\[(x| )\] )(.*)/)
     e.preventDefault()
     checkMark = if match[3] == ' ' then 'x' else ' '
     replaceTo = "#{match[1]}#{match[2]} [#{checkMark}] #{match[4]}"


Scenario 32. Added

Scenario 32. Added

Scenario 32. Two tags with a attribute in the second

--- test/scenario-based-testing.coffee
+++ test/scenario-based-testing.coffee
@@ -334,4 +334,14 @@
     # console.log output
     expectedOutput = fs.readFileSync './test/scenarios/scenario-31-expected.html', 'utf8'
     # console.log expectedOutput
+    expect(output).to.equal(expectedOutput)
+
+  it 'Scenario 32. Two tags with a attribute in the second', ->
+
+    input = fs.readFileSync './test/scenarios/scenario-32-input.cohtml', 'utf8'
+    # console.log input
+    output = toHtml5 input
+    # console.log output
+    expectedOutput = fs.readFileSync './test/scenarios/scenario-32-expected.html', 'utf8'
+    # console.log expectedOutput
     expect(output).to.equal(expectedOutput)


hotfix

hotfix

--- src/js/player.coffee
+++ src/js/player.coffee
@@ -174,7 +174,8 @@
             if @getSongsNum() and @playlist.prev()
                 @trigger('player:prev', {
                     cur: cur
-                }).play()
+                })
+                @play()
             @

         ###*
@@ -191,7 +192,8 @@
                 @trigger('player:next', {
                     auto: auto,
                     cur: cur
-                }).play()
+                })
+                @play()
             @

         ###*


Remove unnecessary option

Remove unnecessary option

This is the default anyway...

--- test/test-app-file-creation.coffee
+++ test/test-app-file-creation.coffee
@@ -11,9 +11,7 @@
     run_context = {}

     beforeEach ->
-        run_context = helpers
-            .run(path.join __dirname, '../app')
-            .withOptions('skip-install': true)
+        run_context = helpers.run(path.join __dirname, '../app')

     context 'with defaults', ->
         beforeEach (done) ->


Make coffeelint happy

Make coffeelint happy

--- src/lib/NoFlo.coffee
+++ src/lib/NoFlo.coffee
@@ -149,7 +149,7 @@
 #         throw err;
 #       }
 #       console.log('Network is now running!');
-#     });
+#     })
 exports.loadFile = (file, options, callback) ->
   unless callback
     callback = options


Always register both types of contest lists for turbolinks

--- resources/assets/coffee/react/contest.coffee
+++ resources/assets/coffee/react/contest.coffee
@@ -16,16 +16,15 @@
 *    along with osu!web.  If not, see <http://www.gnu.org/licenses/>.
 *
 ###
-contest = osu.parseJson('json-contest')
 propsFunction = ->
-  tracks: osu.parseJson('json-tracks')
-  contest: contest
-  options:
-    showDL: contest['type'] == 'beatmap'
-    showPreview: contest['type'] == 'music'
+  contest = osu.parseJson('json-contest')
+  return {
+    tracks: osu.parseJson('json-tracks')
+    contest: contest
+    options:
+      showDL: contest['type'] == 'beatmap'
+      showPreview: contest['type'] == 'music'
+  }

-if contest['type'] == 'art'
-  reactTurbolinks.register 'contestArtList', Contest.ArtList, propsFunction
-
-if contest['type'] == 'beatmap' or contest['type'] == 'music'
-  reactTurbolinks.register 'contestList', Contest.EntryList, propsFunction
+reactTurbolinks.register 'contestArtList', Contest.ArtList, propsFunction
+reactTurbolinks.register 'contestList', Contest.EntryList, propsFunction


class wrapper for popup

class wrapper for popup

--- src/lib/popup.coffee
+++ src/lib/popup.coffee
@@ -7,14 +7,14 @@
 # Private variables
 # -----------------

-# Easily accessible reference to analytics, logging, and the extension
-# controller.
-{analytics, ext, log} = chrome.extension.getBackgroundPage()
+# Easily accessible reference to analytics, logging, utilities, and the
+# extension controller.
+{analytics, ext, log, utils} = chrome.extension.getBackgroundPage()

 # Popup page setup
 # ----------------

-popup = window.popup = new class Popup
+popup = window.popup = new class Popup extends utils.Class

   # Public functions
   # ----------------
@@ -41,5 +41,6 @@
     request =
       data: key: item.getAttribute 'data-key'
       type: item.getAttribute 'data-type'
-    log.debug request
+    log.debug 'Sending the following request to the extension controller',
+      request
     chrome.extension.sendRequest request


Look in all panes for existing preview to show

Previously only the next pane was checked for an existing
preview which would fail to locate any existing previews
that were moved to different panes or were no longer in the
next pane for the edit session.

--- lib/markdown-preview.coffee
+++ lib/markdown-preview.coffee
@@ -15,12 +15,19 @@
       console.warn("Can not render markdown for '#{editSession.getUri() ? 'untitled'}'")
       return

-    if nextPane = activePane.getNextPane()
-      if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}")
-        nextPane.showItem(preview)
-        preview.fetchRenderedMarkdown()
-      else
-        nextPane.showItem(new MarkdownPreviewView(editSession.buffer))
+    {previewPane, previewItem} = @getExistingPreview(editSession)
+    if previewItem?
+      previewPane.showItem(previewItem)
+      previewItem.fetchRenderedMarkdown()
+    else if nextPane = activePane.getNextPane()
+      nextPane.showItem(new MarkdownPreviewView(editSession.buffer))
     else
       activePane.splitRight(new MarkdownPreviewView(editSession.buffer))
     activePane.focus()
+
+  getExistingPreview: (editSession) ->
+    uri = "markdown-preview:#{editSession.getPath()}"
+    for previewPane in rootView.getPanes()
+      previewItem = previewPane.itemForUri(uri)
+      return {previewPane, previewItem} if previewItem?
+    {}


:art: Clean up `providersForScopeDescriptor`

--- lib/provider-manager.coffee
+++ lib/provider-manager.coffee
@@ -53,17 +53,8 @@
           disableDefaultProvider = true

     matchingProviders = _.without(matchingProviders, @fuzzyProvider) if disableDefaultProvider
-    matchingProviders = stableSort matchingProviders, (providerA, providerB) =>
-      specificityA = @metadataForProvider(providerA).getSpecificity(scopeChain)
-      specificityB = @metadataForProvider(providerB).getSpecificity(scopeChain)
-      difference = specificityB - specificityA
-
-      if difference isnt 0
-        difference
-      else
-        (providerB.suggestionPriority ? 1) - (providerA.suggestionPriority ? 1)
-
-    (provider for provider in matchingProviders when (provider.inclusionPriority ? 0) >= lowestIncludedPriority)
+    matchingProviders = (provider for provider in matchingProviders when (provider.inclusionPriority ? 0) >= lowestIncludedPriority)
+    stableSort(matchingProviders, ProviderComparator)

   toggleFuzzyProvider: (enabled) =>
     return unless enabled?
@@ -170,3 +161,10 @@
         disposable.dispose()

     disposable
+
+ProviderComparator = (providerA, providerB) ->
+  specificityA = @metadataForProvider(providerA).getSpecificity(scopeChain)
+  specificityB = @metadataForProvider(providerB).getSpecificity(scopeChain)
+  difference = specificityB - specificityA
+  difference = (providerB.suggestionPriority ? 1) - (providerA.suggestionPriority ? 1) if difference is 0
+  difference


Only fish out the task function once

Only fish out the task function once

--- test/git.coffee
+++ test/git.coffee
@@ -16,6 +16,7 @@

   When -> @subject(@grunt)
   And -> expect(@grunt.registerMultiTask).to.have.been.calledWith 'git', 'A simple API for using git via grunt', sinon.match.func
+  And -> @task = @grunt.registerMultiTask.getCall(0).args[2]

   describe 'command with git options', ->
     Given -> @emitter = new EventEmitter()
@@ -24,7 +25,7 @@
     Given -> @context.options.returns
       message: 'A commit message'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -35,7 +36,7 @@
     Given -> @context.options.returns
       short: true
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -49,7 +50,7 @@
       c: true
       d: 'bar'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -63,7 +64,7 @@
       stdio: 'foo'
       cwd: 'bar'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -74,7 +75,7 @@
     Given -> @context.data =
       cmd: 'remote show origin'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -85,7 +86,7 @@
     Given -> @context.data =
       cmd: 'git remote show origin'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -96,7 +97,7 @@
     Given -> @context.data =
       cmd: 'remote show origin'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -107,7 +108,7 @@
     Given -> @context.options.returns
       useDashes: true
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -118,7 +119,7 @@
     Given -> @context.data =
       rawArgs: '--format=%s'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -129,7 +130,7 @@
     Given -> @context.data =
       rawArgs: ['---blah^foo hi']
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called

@@ -140,6 +141,6 @@
     Given -> @context.options.returns
       'author=': 'nichols'
     When ->
-      @grunt.registerMultiTask.getCall(0).args[2].apply @context, []
+      @task.apply @context, []
       @emitter.emit 'close', 0
     Then -> expect(@cb).to.have.been.called


added tests for UPS getEta

added tests for UPS getEta

--- test/ups.coffee
+++ test/ups.coffee
@@ -134,3 +134,25 @@
         expect(resp).to.equal 'Smuggled Goods'
         done()

+  describe "getEta", ->
+    _presentTimestamp = null
+
+    beforeEach ->
+      _presentTimestamp = bond(_upsClient, 'presentTimestamp').return('at midnight')
+
+    it "uses ScheduledDeliveryDate", ->
+      shipment = 'ScheduledDeliveryDate': ['tomorrow']
+      eta = _upsClient.getEta shipment
+      _presentTimestamp.calledWith('tomorrow').should.equal true
+      expect(eta).to.equal 'at midnight'
+
+    it "uses RescheduledDeliveryDate if ScheduledDeliveryDate is't available", ->
+      shipment = 'Package': ['RescheduledDeliveryDate': ['next week']]
+      _upsClient.getEta shipment
+      _presentTimestamp.calledWith('next week').should.equal true
+
+    it "calls presentTimestamp with a null if shipment is malformed", ->
+      shipment = 'RandomCrap': ['garbage']
+      _upsClient.getEta shipment
+      _presentTimestamp.calledWith().should.equal true
+


update

update


ComboBox: Arrow is not a class member. Removed getter.

--- src/lib/components/combobox/ComboBox.coffee
+++ src/lib/components/combobox/ComboBox.coffee
@@ -35,12 +35,12 @@
       cssClass : 'title'
       template : options.selectionText

-    @arrow = new spark.core.View
+    arrow = new spark.core.View
       cssClass : 'arrow'
       template : 'v'

     @appendView @title
-    @appendView @arrow
+    @appendView arrow

     @menu_.render @getElement()

@@ -296,12 +296,12 @@

   ###*
-    Returns arrow element.
-
-    @export
-  ###
-  getArrow: ->
-    return @arrow
+    Returns title element.
+
+    @export
+  ###
+  getTitle: ->
+    return @title

   ###*


only allow jpg or png uploads

only allow jpg or png uploads

--- src/lib/controllers/image_controller.coffee
+++ src/lib/controllers/image_controller.coffee
@@ -21,6 +21,8 @@
   optimized_path = './image/optimized/' + id + ".jpg"
   thumbnail_path = './image/thumbnail/' + id + ".jpg"

+  allowed_types = {'JPG', 'JPEG', 'PNG'}
+
   description = undefined
   title = undefined
   req.pipe req.busboy
@@ -37,35 +39,40 @@
     file.pipe file_stream
     file_stream.on 'close', () ->
       # Now we need to convert the image to jpg and resize for thumbnails
-      im.convert [original_path, '-resize', '640', optimized_path], (err, stdout) ->
+      im.identify original_path, (err, features) ->
         if err
           return error_exit err
-        # Now make a thumbnail of it too
-        im.convert [original_path, '-thumbnail', '200x200^', '-gravity', 'center',
-                    '-extent', '200x200', thumbnail_path], (err, stdout, stderr) ->
+        if features.format not of allowed_types
+          return error_exit {msg: 'Only \'png\' or \'jpg\' photos may be uploaded at this time.'}
+        im.convert [original_path, '-resize', '640', optimized_path], (err, stdout) ->
           if err
             return error_exit err
-          if not description or not title
-            return error_exit {msg: "Didn't receive description or title."}
-          # Resizing and saving done, now make the image object
-          new_image = models.Image.build {
-            title
-            description
-            image_id: id
-            UserId: req.user.id
-          }
-          new_image.save().success () ->
-            ranking.add_new_image req, new_image.id, (err, reply) ->
-              if err
-                return error_exit err
-              new_image.score_base = reply
-              new_image.save().success () ->
-                req.flash 'success', {msg: 'Upload Successful!'}
-                res.redirect '/image/upload'
-              .failure (err) ->
-                return error_exit err
-          .failure (err) ->
-            return error_exit err
+          # Now make a thumbnail of it too
+          im.convert [original_path, '-thumbnail', '200x200^', '-gravity', 'center',
+                      '-extent', '200x200', thumbnail_path], (err, stdout, stderr) ->
+            if err
+              return error_exit err
+            if not description or not title
+              return error_exit {msg: "Didn't receive description or title."}
+            # Resizing and saving done, now make the image object
+            new_image = models.Image.build {
+              title
+              description
+              image_id: id
+              UserId: req.user.id
+            }
+            new_image.save().success () ->
+              ranking.add_new_image req, new_image.id, (err, reply) ->
+                if err
+                  return error_exit err
+                new_image.score_base = reply
+                new_image.save().success () ->
+                  req.flash 'success', {msg: 'Upload Successful!'}
+                  res.redirect '/image/upload'
+                .failure (err) ->
+                  return error_exit err
+            .failure (err) ->
+              return error_exit err

 get_uploaded = (req, res) ->
   models.User.find(


Fix progress modal style issue

Fix progress modal style issue

--- src/js/controllers/sync-progress-ctrl.coffee
+++ src/js/controllers/sync-progress-ctrl.coffee
@@ -46,7 +46,7 @@
         else if $scope.warning is $scope.total
           'modal-warning'
         else
-
+          'modal-success'

     # Buttons
     close = -> $modalInstance.close


add create new course menu

add create new course menu

--- tutor/specs/current-user-store.spec.coffee
+++ tutor/specs/current-user-store.spec.coffee
@@ -28,6 +28,11 @@
     params: {courseId: '1'}
     label: 'Change Student ID'
   }
+  {
+    name: 'createNewCourse'
+    params: undefined
+    label: 'Add New Course'
+  }
 ]

 TEACHER_MENU = [
@@ -55,6 +60,11 @@
     name: 'courseSettings'
     label: 'Course Settings and Roster'
     params: {courseId: '2'}
+  }
+  {
+    name: 'createNewCourse'
+    params: { offeringId: 1 }
+    label: 'Add New Course'
   }
 ]


whitespace

whitespace

--- src/CaffeineScript/SemanticTree/ClassStn.coffee
+++ src/CaffeineScript/SemanticTree/ClassStn.coffee
@@ -58,8 +58,3 @@
       out + " {}, #{@body.toJs()})"
     else
       out + " {}"
-
-
-
-
-


Use new ubiquitous token format

Use new ubiquitous token format

--- grammar.coffee
+++ grammar.coffee
@@ -13,9 +13,11 @@
 module.exports = compiler.grammar
   name: 'javascript',

-  ubiquitous: ["comment", "_line_break"]
-
-  separators: [' ', '\t', '\r']
+  ubiquitous: -> [
+    @comment,
+    @_line_break,
+    /[ \t\r]/
+  ]

   rules:
     program: -> repeat(@statement)


fix(deprecate-utils): Add location where deprecation warning was issued

- Resolves #1599

--- src/deprecate-utils.coffee
+++ src/deprecate-utils.coffee
@@ -1,6 +1,16 @@
 _ = require 'underscore'

 class DeprecateUtils
+  # See
+  # http://www.codeovertones.com/2011/08/how-to-print-stack-trace-anywhere-in.html
+  @parseStack: (stackString) ->
+    stack = stackString
+      .replace(/^[^\(]+?[\n$]/gm, '')
+      .replace(/^\s+at\s+/gm, '')
+      .replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@')
+      .split('\n')
+    return stack
+
   @warn: (condition, message) ->
     console.warn message if condition

@@ -8,10 +18,11 @@
     if NylasEnv.inDevMode() and not NylasEnv.inSpecMode()
       warn = true
       newFn = =>
+        stack = DeprecateUtils.parseStack((new Error()).stack)
         DeprecateUtils.warn(
           warn,
           "Deprecation warning! #{fnName} is deprecated and will be removed soon.
-          Use #{newName} instead."
+          Use #{newName} instead.\nCheck your code at #{stack[1]}"
         )
         warn = false
         return fn.apply(ctx, arguments)


Add tests for compareVersions().

Add tests for compareVersions().

--- tests/unit_tests/utils_test.coffee
+++ tests/unit_tests/utils_test.coffee
@@ -99,9 +99,14 @@
 context "compare versions",
   should "compare correctly", ->
     assert.equal 0, Utils.compareVersions("1.40.1", "1.40.1")
+    assert.equal 0, Utils.compareVersions("1.40", "1.40.0")
+    assert.equal 0, Utils.compareVersions("1.40.0", "1.40")
     assert.equal -1, Utils.compareVersions("1.40.1", "1.40.2")
     assert.equal -1, Utils.compareVersions("1.40.1", "1.41")
+    assert.equal -1, Utils.compareVersions("1.40", "1.40.1")
     assert.equal 1, Utils.compareVersions("1.41", "1.40")
+    assert.equal 1, Utils.compareVersions("1.41.0", "1.40")
+    assert.equal 1, Utils.compareVersions("1.41.1", "1.41")

 context "makeIdempotent",
   setup ->


Added support for video textures / windows

Works on desktop well, works with oculus, having rendering issues on
mobile (htc one and nexus 7)

--- src/core/Firecracker.coffee
+++ src/core/Firecracker.coffee
@@ -154,6 +154,32 @@
         floor.position.y -= 10

         return floor
+
+
+    VideoTexture: ( video_src ) =>
+        video = document.createElement("video");
+        video.src = video_src
+        video.style = "display:none; position:absolute; top:1px; left:0;"
+        video.autoplay = true
+        video.loop = true
+
+        videoTexture = new THREE.Texture( video )
+        videoTexture.minFilter = THREE.LinearFilter
+        videoTexture.magFilter = THREE.LinearFilter
+
+        video_object = {
+
+            material: new THREE.MeshBasicMaterial({
+                map: videoTexture
+                overdraw: true
+                side:THREE.DoubleSide
+            })
+
+            update: () =>
+                setTimeout( ( () => videoTexture.needsUpdate = true ), 4000 )
+        }
+
+        return video_object
 }


removing unused utilities.js

removing unused utilities.js

--- helpers.coffee
+++ helpers.coffee
@@ -28,7 +28,6 @@
   extend (extend {}, options), overrides

 # Extend a source object with the properties of another object (shallow copy).
-# We use this to simulate Node's deprecated `process.mixin`.
 extend = exports.extend = (object, properties) ->
   for all key, val of properties
     object[key] = val


Add (ignored for now) tests for params.setHash on set parameters.

--- spec/hashparamsSpec.coffee
+++ spec/hashparamsSpec.coffee
@@ -108,6 +108,22 @@
                 for name, hash of emptyCases
                     do (name, hash) ->
                         it name, -> hashYieldsValues hash, {tags: setOf(), authors: setOf()}
+            xit 'can set values.tags to an empty Set', ->
+                hashYieldsValues '#tags=', {tags: setOf(), authors: setOf()}
+            xit 'can set values.tags to a single value', ->
+                hashYieldsValues '#tags=A', {tags: setOf('A'), authors: setOf()}
+            xit 'can set values.tags to a multiple values', ->
+                hashYieldsValues '#tags=A,B,C', {tags: setOf('A', 'B', 'C'), authors: setOf()}
+            xit 'clears other values', ->
+                params.values.tags = setOf('A', 'B', 'C')
+                hashYieldsValues '#authors=Bob', {tags: setOf(), authors: setOf('Bob')}
+            xit 'can set both values', ->
+                hashYieldsValues '#tags=A,B,C;authors=Bob,Ned',
+                    {tags: setOf('A', 'B', 'C'), authors: setOf('Bob', 'Ned')}
+            xit 'can take values with encoded commas', ->
+                hashYieldsValues '#tags=A%2CB,C%2CD', {tags: setOf('A,B', 'C,d'), authors: setOf()}
+            it 'will not set values.foo', ->
+                hashYieldsValues '#foo=bar', {tags: setOf(), authors: setOf()}
         describe 'unencoding', ->
             decodes = (encoded, decoded) ->
                 params = new HashParams('name' + decoded)


add doc-ready

add doc-ready

--- app/assets/javascripts/suspicious_votes.coffee
+++ app/assets/javascripts/suspicious_votes.coffee
@@ -2,10 +2,14 @@
 # All this logic will automatically be available in application.js.
 # You can use CoffeeScript in this file: http://coffeescript.org/

-$('a[data-remote]').on('ajax:success', (e, data, status, xhr) ->
-  $(this).parents('tr').fadeOut(200, () ->
-    $(this).remove()
+$(document).on('ready page:load', () ->
+
+  $('a[data-remote]').on('ajax:success', (e, data, status, xhr) ->
+    $(this).parents('tr').fadeOut(200, () ->
+      $(this).remove()
+    )
+  ).on('ajax:error', (e, xhr, status, error) ->
+    alert('Failed to mark this investigation completed.')
   )
-).on('ajax:error', (e, xhr, status, error) ->
-  alert('Failed to mark this investigation completed.')
+
 )


Fixed typos

Fixed typos

--- scripts/gh-book/app.coffee
+++ scripts/gh-book/app.coffee
@@ -50,8 +50,8 @@
       # Show the WelcomeView's settings modal if there was a connection problem
       try
         App.main.show(welcomeView)
-        welcomeView.editSettingsModal(msg)
-      catch:
+        welcomeView.editSettingsModal(message)
+      catch err
         alert("#{message} Are you pointing to a valid book? Using github/#{repoUser}/#{repoName}#{branch}")


Refactor common code to load the remote updater

--- scripts/gh-book/app.coffee
+++ scripts/gh-book/app.coffee
@@ -131,7 +131,15 @@

             '':             'goWorkspace' # Show the workspace list of content
             'workspace':    'goWorkspace'
-            'edit/*id':     'goEdit' # Edit an existing piece of content (id can be a path)
+            'edit/:id':     'goEdit' # Edit an existing piece of content (id can be a URL-encoded path)
+
+          _loadFirst: () ->
+            setDefaultRepo()
+            return remoteUpdater.start()
+            .fail((err) => alert('There was a problem starting the remote updater. Are you pointing to a valid book?'))
+            .then () =>
+              allContent.load()
+              .fail((err) => alert('There was a problem loading the repo. Are you pointing to a valid book?'))

           configRepo: (repoUser, repoName, branch='') ->
             session.set
@@ -146,21 +154,9 @@
           # Delay the route handling until the initial content is loaded
           # TODO: Move this into the controller
           goWorkspace: () ->
-            setDefaultRepo()
-            remoteUpdater.start()
-            .fail((err) => alert('There was a problem starting the remote updater. Are you pointing to a valid book?'))
-            .done () =>
-              allContent.load()
-              .fail((err) => alert('There was a problem loading the repo. Are you pointing to a valid book?'))
-              .done () => controller.goWorkspace()
+            @_loadFirst().done () => controller.goWorkspace()
           goEdit: (id)    ->
-            setDefaultRepo()
-            remoteUpdater.start()
-            .fail((err) => alert('There was a problem starting the remote updater. Are you pointing to a valid book?'))
-            .done () =>
-              allContent.load()
-              .fail((err) => alert('There was a problem loading the repo. Are you pointing to a valid book?'))
-              .done () => controller.goEdit(id)
+            @_loadFirst().done () => controller.goEdit(id)

         Backbone.history.start


Put an adapter on it

Put an adapter on it

--- scripts/poo-tracker.coffee
+++ scripts/poo-tracker.coffee
@@ -6,7 +6,6 @@
 redis_client.auth info.auth.split(":")[1] if info.auth

 module.exports = (robot) ->
-  console.log("Poop boot is #{robot.constructor.name} adapter: #{robot.adapter.send}")
   POO_TRACKER_KEY = "poops"
   POO_LATEST_KEY = "poops:latest_message"

@@ -20,7 +19,7 @@
       envelope.user = {}
       envelope.user.room = envelope.room = room
       envelope.user.type = 'groupchat'
-      robot.send envelope, reply
+      robot.adapter.send envelope, reply
       redis_client.set POO_LATEST_KEY, reply

   robot.respond /poo tracker( me)?/i, (res) ->
@@ -29,7 +28,7 @@
       return if reply == null
       return res.send "Latest poo: #{reply}"

-  robot.on 'connected', ->
+  robot.adapter.on 'connected', ->
     console.log("Connected event for poop tracker")
     setInterval ->
       checkRedisForShit()


collsions

collsions

--- app/both/collections/Tanks.coffee
+++ app/both/collections/Tanks.coffee
@@ -70,6 +70,17 @@
     @msg("shoot #{vec}")

   collide: (item) ->
+    OBJSIZE = 100
+    rect1 = {x: @left, y: @top, width: OBJSIZE, height: OBJSIZE}
+    rect2 = {x: item.left, y: item.top, width: OBJSIZE, height: OBJSIZE}
+
+    if rect1.x < rect2.x + rect2.width &&
+       rect1.x + rect1.width > rect2.x &&
+       rect1.y < rect2.y + rect2.height &&
+       rect1.height + rect1.y > rect2.y
+      console.log("collision")
+      return true
+
     return false


fix case where next frame btn is sometimes shown in 4up

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -112,7 +112,11 @@

     asteroidTool:
       enter: ->
-        if @el.attr('flicker') is 'true' then @activateFrame 0 else @showAllTrackingIcons()
+        if @el.attr('flicker') is 'true'
+          @activateFrame 0
+        else
+          @showAllTrackingIcons()
+          @nextFrame.hide()
         @enableMarkingSurfaces()
         @currSighting = new Sighting({type:"asteroid"})
         @el.find('.asteroid-classifier').show()


removed old code

removed old code

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -59,10 +59,8 @@
     super
     @html @template

-    # Initialize QuickGuide
+    # Initialize QuickGuide and FITS Viewer
     @quickGuide = new QuickGuide({el: @el.find('.quick-guide')})
-
-    # Initialize controller for WebFITS
     @viewer = new Viewer({el: @el.find('.viewer')[0], classifier: @})

     # Setup events
@@ -78,6 +76,7 @@
       content: "That's a lot of markers! Remember, lenses are rare."
       attachment: 'center center svg.primary center center'

+    # Reset variables before new classification
     @reset()

     # Start with subject group
@@ -94,7 +93,6 @@
     @annotations      = {}
     @annotationIndex  = 0
     @annotationCount  = 0
-    @warn             = true
     @hasAnnotation    = false
     @hasWarned        = false
     @preset           = null
@@ -103,7 +101,6 @@
     @yDown            = null
     @toAnnotate       = true

-    @dashboardTutorial = false
     @isTrainingSubject = false
     @ctx = undefined

@@ -127,12 +124,6 @@
         @nPotentials  = project.annotation_count or 0
         @nFavorites   = project.favorite_count or 0
         @trigger 'start'
-
-      # Determine if dashboard tutorial completed
-      console.log 'dashboard_tutorial_done', 'dashboard_tutorial_done' of project
-
-      unless 'tutorial_dashboard' of project
-        @trigger 'dashboard-tutorial'
     else
       @trigger 'tutorial'

@@ -274,9 +265,8 @@

   onSubjectSelect: (e, subject) =>

+    # Reset classification variables and create new classification
     @reset()
-
-    # Create new classification
     @classification = new Classification {subject}

     # Update DOM


Classifier triggers resize on activate

Classifier triggers resize on activate

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -10,6 +10,7 @@
 {Tutorial} = require 'zootorial'
 getTutorialSubject = require '../lib/get-tutorial-subject'
 tutorialSteps = require '../lib/tutorial-steps'
+$ = require 'jqueryify'

 class Classifier extends Controller
   surface: null
@@ -103,4 +104,8 @@
   onNoMoreSubjects: =>
     @el.removeClass 'loading'

+  activate: ->
+    super
+    $(window).trigger 'resize'
+
 module.exports = Classifier


sim freq text

sim freq text

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -555,19 +555,16 @@

   # Training scheme using simulations and empty fields
   setSimulationRatio: ->
-    if true
-      # When duds are included in the @TrainingGroup, need to multiply @simratio by 2 to
-      baseLevel = Math.floor(@nClassified / 20) + 1
-      @level = Math.min(baseLevel, 3)
-      @simRatio = 1 / (5 * Math.pow(Math.sqrt(2), @level - 1))
-      Subject.group = if @simRatio > Math.random() then @simulationGroup else @subjectGroup
-      # console.log "#{@nClassified}, #{@level}, #{@simRatio}, #{Subject.group}"
-    else
-      # NOTE: This is the old scheme with fall off of 1 / x
-      @simRatio = Math.floor(@nClassified / 20) + 2
-      Subject.group = if @nClassified % @simRatio is 0 then @simulationGroup else @subjectGroup
-      # console.log "#{@simRatio}, #{Subject.group}"
-    @simFrequency.text("#{@simRatio.toFixed(2)}")
+
+    # When duds are included in the @TrainingGroup, need to multiply @simratio by 2 to
+    baseLevel = Math.floor(@nClassified / 20) + 1
+    @level = Math.min(baseLevel, 3)
+    denominator = (5 * Math.pow(Math.sqrt(2), @level - 1))
+    @simRatio = 1 / denominator
+    Subject.group = if @simRatio > Math.random() then @simulationGroup else @subjectGroup
+
+    # Update sim freq text
+    @simFrequency.text("1 in #{denominator}")

   submit: (e) =>

@@ -637,7 +634,7 @@
       training = @classification.subject.metadata.training
       trainingType = training.type

-      if trainingType in ['lensed galaxy', 'lensed quasar']
+      if trainingType in ['lensing cluster', 'lensed quasar', 'lensed galaxy']

         # Get the location for the dialog
         x = (training.x + 30) / @subjectDimension


remove invite user when clicking on the line. only when clicking the button

--- app/controllers/users_list.coffee
+++ app/controllers/users_list.coffee
@@ -39,13 +39,12 @@
     element = $(e.currentTarget)
     item = element.data('item')
     @log 'UsersList::click ', item, item.id
-    if item instanceof GmailUser
-      if confirm("Would you like to invite #{item.name} (#{item.email}}")
-        @invite(item)
-      return
+    # Do not go into detail view for GmailUsers. We don't have enough details to show anything interesting
+    return if item instanceof GmailUser
     @navigate(@constructor.item_url, item.id, trans: 'right')

   invite: (item) ->
+    return unless confirm("Would you like to invite #{item.name} (#{item.email}}")
     Spine.trigger 'notify', msg: 'Sending invite email to ' + item.email
     Authorization.friendAjax('invites', item.id)