             levelID: level.original
             slug: level.slug
             courseSlug: courses.get(course._id).get('slug')
+          for intro in level.introContent ? [] when intro.type is 'interactive'
+            # TODO: this only works for Python presently
+            courseInteractives.push
+              courseIndex: index + 1
+              interactiveID: intro.contentId.python ? intro.contentId
+              courseSlug: courses.get(course._id).get('slug')
       users = new Users()
       Promise.resolve($.when(users.fetchForClassroom(classroom)...))
     .then (models) =>

          acronym = switch
            when /game-dev/.test(level.courseSlug) then 'GD'
            when /web-dev/.test(level.courseSlug) then 'WD'
            else 'CS'
          lastCourseLabel = acronym + ++courseLabelIndexes[acronym]
        columnLabels += ",#{lastCourseLabel}.#{currentLevel++} #{level.slug}"
      csvContent = "data:text/csv;charset=utf-8,#{columnLabels}\n"
      for studentRow in userPlaytimes
        csvContent += studentRow.join(',') + "\n"
      csvContent = csvContent.substring(0, csvContent.length - 1)
      encodedUri = encodeURI(csvContent)

      sessions = new LevelSessions()
      Promise.resolve($.when(sessions.fetchForAllClassroomMembers(classroom)...))
    .then (models) =>
      for session in sessions.models
        continue unless session.get('state')?.complete
        levelID = session.get('level').original
        userID = session.get('creator')
        userLevelPlaytimeMap[userID] ?= {}
        userLevelPlaytimeMap[userID][levelID] ?= {}
        if session.get('contentPlaytimes')
          playtime = 0
          playtime += content.playtime ? 0 for content in session.get('contentPlaytimes')
        else
          playtime = session.get('playtime')
        userLevelPlaytimeMap[userID][levelID] = playtime
      interactiveSessions = new InteractiveSessions()
      #Promise.resolve($.when(interactiveSessions.fetchForAllClassroomMembers(classroom)...))
      Promise.resolve([])  # No interactives in CodeCombat yet
    .then (models) =>
      userInteractiveAttemptMap = {}
      for session in interactiveSessions.models
        continue unless session.get('complete')
        interactiveID = session.get('interactiveId')
        userID = session.get('userId')
        userInteractiveAttemptMap[userID] ?= {}
        userInteractiveAttemptMap[userID][interactiveID] ?= {}
        userInteractiveAttemptMap[userID][interactiveID] = session.get('submissionCount')

      userRows = []
      for userID, user of userMap
        row = [user.get('name') ? 'Anonymous']
        for level in courseLevels
          if userLevelPlaytimeMap[userID]?[level.levelID]?
            rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID])
            if false
              # Old way, with human-readable times
              hours = Math.floor(rawSeconds / 60 / 60)
              minutes = Math.floor(rawSeconds / 60 - hours * 60)
              seconds = Math.round(rawSeconds - hours * 60 - minutes * 60)
              hours = "0#{hours}" if hours < 10
              minutes = "0#{minutes}" if minutes < 10
              seconds = "0#{seconds}" if seconds < 10
              row.push "#{hours}:#{minutes}:#{seconds}"
            else
              # New way, with machine-analyzable times (seconds)
              row.push Math.round(rawSeconds)
          else
            row.push 'Incomplete'

        for interactive in courseInteractives
          attempts = userInteractiveAttemptMap[userID]?[interactive.interactiveID]
          if attempts
            row.push attempts
          else
            row.push 'Incomplete'

        userRows.push(row)

      columnLabels = "Username"
      currentLevel = 1
      courseLabelIndexes = CS: 1, GD: 0, WD: 0, CH: 1
      lastCourseIndex = 1
      #lastCourseLabel = 'CH1'  # TODO: differentiate products
      lastCourseLabel = 'CS1'
      for level in courseLevels
        unless level.courseIndex is lastCourseIndex
