This commit is contained in:
Nick Winter 2015-03-07 16:30:25 -08:00
parent 6954175fa8
commit e21360127d
35 changed files with 931 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

@ -69,6 +69,8 @@ module.exports = class CocoRouter extends Backbone.Router
'editor/thang': go('editor/thang/ThangTypeSearchView')
'editor/thang/:thangID': go('editor/thang/ThangTypeEditView')
'editor/campaign/:campaignID': go('editor/campaign/CampaignEditorView')
'editor/poll': go('editor/poll/PollSearchView')
'editor/poll/:articleID': go('editor/poll/PollEditView')
'employers': go('EmployersView')
@ -82,6 +84,7 @@ module.exports = class CocoRouter extends Backbone.Router
'i18n/level/:handle': go('i18n/I18NEditLevelView')
'i18n/achievement/:handle': go('i18n/I18NEditAchievementView')
'i18n/campaign/:handle': go('i18n/I18NEditCampaignView')
'i18n/poll/:handle': go('i18n/I18NEditPollView')
'legal': go('LegalView')

View file

@ -64,6 +64,7 @@
achievements: "Achievements" # Tooltip on achievement list button from /play
account: "Account" # Tooltip on account button from /play
settings: "Settings" # Tooltip on settings button from /play
poll: "Poll" # Tooltip on poll button from /play
next: "Next" # Go from choose hero to choose inventory before playing a level
change_hero: "Change Hero" # Go back from choose inventory to choose hero
choose_inventory: "Equip Items"
@ -727,6 +728,7 @@
thang_title: "Thang Editor"
level_title: "Level Editor"
achievement_title: "Achievement Editor"
poll_title: "Poll Editor"
back: "Back"
revert: "Revert"
revert_models: "Revert Models"
@ -787,10 +789,13 @@
new_level_title_login: "Log In to Create a New Level"
new_achievement_title: "Create a New Achievement"
new_achievement_title_login: "Log In to Create a New Achievement"
new_poll_title: "Create a New Poll"
new_poll_title_login: "Log In to Create a New Poll"
article_search_title: "Search Articles Here"
thang_search_title: "Search Thang Types Here"
level_search_title: "Search Levels Here"
achievement_search_title: "Search Achievements"
poll_search_title: "Search Polls"
read_only_warning2: "Note: you can't save any edits here, because you're not logged in."
no_achievements: "No achievements have been added for this level yet."
achievement_query_misc: "Key achievement off of miscellanea"
@ -804,6 +809,9 @@
edit_btn_preview: "Preview"
edit_article_title: "Edit Article"
polls:
priority: "Priority"
contribute:
page_title: "Contributing"
intro_blurb: "CodeCombat is 100% open source! Hundreds of dedicated players have helped us build the game into what it is today. Join us and write the next chapter in CodeCombat's quest to teach the world to code!"
@ -1052,6 +1060,8 @@
feedback: "Feedback"
payment_info: "Payment Info"
campaigns: "Campaigns"
poll: "Poll"
user_polls_record: "Poll Voting History"
delta:
added: "Added"

8
app/models/Poll.coffee Normal file
View file

@ -0,0 +1,8 @@
CocoModel = require './CocoModel'
schema = require 'schemas/models/poll.schema'
module.exports = class Poll extends CocoModel
@className: 'Poll'
@schema: schema
urlRoot: '/db/poll'
saveBackups: true

View file

@ -0,0 +1,7 @@
CocoModel = require './CocoModel'
schema = require 'schemas/models/user-polls-record.schema'
module.exports = class UserPollsRecord extends CocoModel
@className: 'UserPollsRecord'
@schema: schema
urlRoot: '/db/user.polls.record'

View file

@ -9,6 +9,7 @@ ArticleSchema.properties.i18n = {type: 'object', title: 'i18n', format: 'i18n',
c.extendBasicProperties ArticleSchema, 'article'
c.extendSearchableProperties ArticleSchema
c.extendVersionedProperties ArticleSchema, 'article'
c.extendTranslationCoverageProperties ArticleSchema
c.extendPatchableProperties ArticleSchema
module.exports = ArticleSchema

View file

@ -0,0 +1,23 @@
c = require './../schemas'
PollSchema = c.object {title: 'Poll'}
c.extendNamedProperties PollSchema # name first
_.extend PollSchema.properties,
description: {type: 'string', title: 'Description', description: 'Optional: extra context or explanation', format: 'markdown' }
answers: c.array {title: 'Answers'},
c.object {required: ['key', 'text', 'i18n', 'votes']},
key: c.shortString {title: 'Key', description: 'Key for recording votes, like 14-to-17', pattern: '^[a-z0-9-]+$'}
text: c.shortString {title: 'Text', description: 'Answer that the player will see, like 14 - 17.', format: 'markdown'}
i18n: {type: 'object', title: 'i18n', format: 'i18n', props: ['text']}
votes: {title: 'Votes', type: 'integer', minimum: 0}
i18n: {type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'description']}
created: c.isodate {title: 'Created', readOnly: true}
priority: {title: 'Priority', description: 'Lower numbers will show earlier.', type: 'integer'}
c.extendBasicProperties PollSchema, 'poll'
c.extendSearchableProperties PollSchema
c.extendTranslationCoverageProperties PollSchema
c.extendPatchableProperties PollSchema
module.exports = PollSchema

View file

@ -0,0 +1,20 @@
c = require './../schemas'
UserPollsRecordSchema = c.object {title: 'UserPollsRecord'}
_.extend UserPollsRecordSchema.properties,
user: c.stringID {links: [{rel: 'extra', href: '/db/user/{($)}'}]}
polls: # Poll ID strings -> answer key strings
type: 'object'
additionalProperties: c.shortString {pattern: '^[a-z0-9-]+$'}
rewards: # Poll ID strings -> reward objects, for calculating gems
type: 'object'
additionalProperties: c.object {},
random: {type: 'number', minimum: 0, maximum: 1}
level: {type: 'integer', minimum: 1}
level: {type: 'integer', minimum: 1, description: 'The player level when last saved.'}
changed: c.isodate title: 'Changed', readOnly: true # Controls when next poll is available
c.extendBasicProperties UserPollsRecordSchema, 'user-polls-record'
module.exports = UserPollsRecordSchema

View file

@ -15,10 +15,13 @@ me.object = (ext, props) -> combine({type: 'object', additionalProperties: false
me.array = (ext, items) -> combine({type: 'array', items: items or {}}, ext)
me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext)
me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext)
# Dates should usually be strings, ObjectIds should be strings: https://github.com/codecombat/codecombat/issues/1384
me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext)
# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient
me.objectId = (ext) -> schema = combine({type: ['object', 'string']}, ext)
me.stringID = (ext) -> schema = combine({type: 'string', minLength: 24, maxLength: 24}, ext)
me.isodate = (ext) -> combine({type: ['object'], format: 'date-time'}, ext) # use for server-side-only dates?
me.objectId = (ext) -> schema = combine({type: ['object', 'string']}, ext) # old
me.stringID = (ext) -> schema = combine({type: 'string', minLength: 24, maxLength: 24}, ext) # use for anything new
me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext)
me.int = (ext) -> combine {type: 'integer'}, ext
me.float = (ext) -> combine {type: 'number'}, ext

View file

@ -0,0 +1,17 @@
#editor-poll-edit-view
.treema-root
margin: 28px 0px 20px
.poll-tool-button
float: right
margin-top: 15px
margin-left: 10px
textarea
width: 92%
height: 300px
#poll-view
min-height: 200px
position: relative

View file

@ -336,7 +336,7 @@ $gameControlMargin: 30px
&:hover
@include box-shadow(0 0 12px #bbf)
&:active
&:active, &.highlighted
@include box-shadow(0 0 20px white)
&.heroes
@ -344,11 +344,14 @@ $gameControlMargin: 30px
&.achievements
background-position: (-2 * $gameControlSize) 0px
&.account
background-position: (-3 * $gameControlSize) 0px
//background-position: (-3 * $gameControlSize) 0px
background-position: (-4 * $gameControlSize) 0px
&.settings
background-position: (-4 * $gameControlSize) 0px
&.gems
background-position: (-5 * $gameControlSize) 0px
&.poll
background-position: (-3 * $gameControlSize) 0px
.tooltip
font-size: 24px

View file

@ -0,0 +1,190 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
#poll-modal
$hero-yellow-text: rgb(252, 201, 53)
//- Top-level modal container
.modal-dialog
margin-top: 0
padding-top: 0
width: 750px
.modal-content
position: relative
margin-top: -251px
@media only screen and (max-height: 720px)
.modal-dialog
margin-top: -76px
//- Header
.background-wrapper
//background: url("/images/pages/play/level/modal/victory_modal_background.png")
width: 750px
background: transparent
border: 0px solid transparent
border-style: solid
border-image: url("/images/pages/play/level/modal/victory_modal_border_background.png") 250 0 100 0 fill round
border-width: 250px 0 100px 0
border-radius: 12px
.modal-header
border: none
position: absolute
left: 188px
width: 378px
height: 134px
margin: 0
padding: 0
.close
display: none
h1
position: absolute
left: 0
top: 90px
margin: 0
width: 380px
text-align: center
color: rgb(254,188,68)
font-size: 32px
text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0
//- Body
.modal-body
padding: 0 20px
min-height: 30px
margin-top: 160px
.description
margin: 20px 5px 0 5px
color: white
text-align: center
.answers-container-wrapper
margin-top: 5px
border: 4px solid rgb(26, 21, 17)
.answers-container
background-color: rgb(45, 36, 29)
border: 4px solid rgb(74, 61, 48)
padding: 15px
table.table
margin-bottom: 0
tr.answer
&:not(.selected) td
background-color: rgb(74, 61, 48)
border-color: lighten(rgb(74, 61, 48), 10%)
&:hover:not(.selected) td
background-color: lighten(rgb(74, 61, 48), 10%)
&.selected td
background-color: rgb(33, 28, 21)
border-color: lighten(rgb(33, 28, 21), 10%)
&:not(.selected)
cursor: pointer
td
vertical-align: middle
code
padding: 2px 4px
font-size: 90%
color: white
background-color: #333
border-radius: 3px
@include box-shadow(inset 0 -1px 0 rgba(0, 0, 0, .25))
&.graph-cell
min-width: 200px
p
margin: 0
color: white
.progress
width: 100%
margin-bottom: 0
background-color: rgb(45, 36, 29)
border-radius: 10px
.progress-bar
background-color: rgb(245, 170, 49)
@include transition(none)
&.votes-cell
max-width: 34px
.vote-percentage.badge
background-color: rgb(245, 170, 49)
text-shadow: -1px -1px 0px black, 1px 1px 0px black, -1px 1px 0px black, 1px -1px 0px black
table:not(.answered)
tr
text-align: center
.graph-cell, .votes-cell
display: none
.random-gems-container-wrapper
width: 558px
height: 115px
background: transparent url(/images/pages/play/modal/random-gems-background.png) no-repeat 100% 100%
padding: 25px
margin: 10px auto
&:not(.answered)
display: none
.random-gems-container
.random-gems-code
font-size: 14px
display: block
white-space: pre
padding: 2px 4px
font-size: 90%
color: black
background-color: transparent
border-radius: 0
margin-bottom: 5px
.comment
font-weight: bold
color: darken(rgb(245, 170, 49), 30%)
//- Footer - other stuff
.modal-footer
// Negative bottom margin counteracts most of the extra the border image height.
margin: 0 0 -80px 0
padding: 0 20px
text-align: center
.done-button
float: right
height: 60px
min-width: 100px
line-height: 30px
margin: 0 10px
html.no-borderimage
#poll-modal
.modal-dialog
margin-top: 251px
.background-wrapper
border: 0
background: url("/images/pages/play/level/modal/victory_modal_background.png")
height: 713px
@media only screen and (max-height: 720px)
.modal-dialog
margin-top: 175px

View file

@ -8,7 +8,7 @@ block tableHeader
block tableBody
for data in documents
- data = data.attributes
- data = data.attributes;
tr
td
a(href="/editor/achievement/#{data.slug || data._id}")

View file

@ -0,0 +1,31 @@
extends /templates/base
block content
if !unauthorized
ol.breadcrumb
li
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
li
a(href="/editor/poll", data-i18n="editor.poll_title") Poll Editor
li.active
| #{poll.attributes.name}
button.poll-tool-button(data-i18n="common.delete", disabled=!me.isAdmin()).btn.btn-primary#delete-button Delete
button.poll-tool-button(data-i18n="common.save", disabled=!me.isAdmin()).btn.btn-primary#save-button Save
h3(data-i18n="poll.edit_poll_title") Edit Poll
span
|: "#{poll.attributes.name}"
#poll-treema
#poll-view.clearfix
h3(data-i18n="resources.patches") Patches
.patches-view
hr
else
.alert.alert-danger
span Admin only. Turn around.

View file

@ -0,0 +1,19 @@
extends /templates/common/table
block tableHeader
tr
th(data-i18n="general.name") Name
th(data-i18n="general.description") Description
th(data-i18n="polls.priority") Priority
th(data-i18n="general.date") Date
block tableBody
for data in documents
- data = data.attributes;
tr
td
a(href="/editor/poll/#{data.slug || data._id}")
| #{data.name}
td= data.description
td= data.priority
td= moment(data.created).fromNow()

View file

@ -78,6 +78,7 @@ else
btn(data-i18n="common.play").btn.btn-illustrated.btn-lg.btn-success.play-button
.game-controls.header-font
button.btn.poll.hidden(data-i18n="[title]play.poll")
button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes")
button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
@ -85,9 +86,9 @@ else
button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
if !me.get('anonymous', true)
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
if me.isAdmin()
button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
else if me.get('anonymous', true)
//if me.isAdmin()
// button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
if me.get('anonymous', true)
button.btn.settings(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="[title]play.settings")
.user-status.header-font

View file

@ -0,0 +1,40 @@
extends /templates/core/modal-base
block modal-header-content
h1
span(data-i18n="play.poll") Poll
span.spr :
span= i18n(poll.attributes, "name")
block modal-body-content
.description
if poll.get("description")
div!= marked(i18n(poll.attributes, "description"))
else
div  
.answers-container-wrapper
.answers-container
table.table.table-hover
for answer in poll.get("answers")
tr(class="answer", data-answer=answer.key)
td!= marked(i18n(answer, "text"))
td.graph-cell
.progress
.progress-bar
td.votes-cell
span.badge.vote-percentage
.random-gems-container-wrapper
.random-gems-container
code.random-gems-code
span randomNumber = Math.random()
span.comment#random-number-comment
code.random-gems-code
span gems = Math.ceil(2 * randomNumber * me.level)
span.comment#random-gems-comment
code.random-gems-code
span me.gems += gems
span.comment#total-gems-comment
block modal-footer-content
button.btn.btn-illustrated.btn-lg.done-button(data-dismiss="modal", aria-hidden="true", data-i18n="play_level.done") Done

View file

@ -77,7 +77,7 @@ module.exports = class SearchView extends RootView
@hideLoading()
@collection.sort()
documents = @collection.models
table = $(@tableTemplate(documents: documents, me: me, page: @page))
table = $(@tableTemplate(documents: documents, me: me, page: @page, moment: moment))
@$el.find('table').replaceWith(table)
@$el.find('table').i18n()

View file

@ -16,5 +16,4 @@ module.exports = class AchievementSearchView extends SearchView
context.currentSearch = 'editor.achievement_search_title'
context.newModelsAdminOnly = true
context.unauthorized = true unless me.isAdmin() or me.isArtisan()
@$el.i18n()
context

View file

@ -0,0 +1,132 @@
RootView = require 'views/core/RootView'
template = require 'templates/editor/poll/poll-edit-view'
Poll = require 'models/Poll'
UserPollsRecord = require 'models/UserPollsRecord'
PollModal = require 'views/play/modal/PollModal'
ConfirmModal = require 'views/editor/modal/ConfirmModal'
PatchesView = require 'views/editor/PatchesView'
errors = require 'core/errors'
app = require 'core/application'
module.exports = class PollEditView extends RootView
id: 'editor-poll-edit-view'
template: template
events:
'click #save-button': 'savePoll'
'click #delete-button': 'confirmDeletion'
constructor: (options, @pollID) ->
super options
@loadPoll()
@loadUserPollsRecord()
@pushChangesToPreview = _.throttle(@pushChangesToPreview, 500)
loadPoll: ->
@poll = new Poll _id: @pollID
@poll.saveBackups = true
@supermodel.loadModel @poll, 'poll'
loadUserPollsRecord: ->
url = "/db/user.polls.record/-/user/#{me.id}"
@userPollsRecord = new UserPollsRecord().setURL url
onRecordSync = ->
return if @destroyed
@userPollsRecord.url = -> '/db/user.polls.record/' + @id
@listenToOnce @userPollsRecord, 'sync', onRecordSync
@userPollsRecord = @supermodel.loadModel(@userPollsRecord, 'user_polls_record').model
onRecordSync.call @ if @userPollsRecord.loaded
onLoaded: ->
super()
@buildTreema()
buildTreema: ->
return if @treema? or (not @poll.loaded)
data = $.extend(true, {}, @poll.attributes)
options =
data: data
filePath: "db/poll/#{@poll.get('_id')}"
schema: Poll.schema
readOnly: me.get('anonymous')
callbacks:
change: => @pushChangesToPreview() unless @hush
@treema = @$el.find('#poll-treema').treema(options)
@treema.build()
@treema.childrenTreemas.answers?.open 1
@pushChangesToPreview()
getRenderData: (context={}) ->
context = super(context)
context.poll = @poll
context.authorized = me.isAdmin()
context
afterRender: ->
super()
return unless @supermodel.finished()
@pushChangesToPreview()
@patchesView = @insertSubView(new PatchesView(@poll), @$el.find('.patches-view'))
@patchesView.load()
pushChangesToPreview: =>
return unless @treema
@$el.find('#poll-view').empty()
for key, value of @treema.data
@poll.set key, value
@pollModal?.destroy()
@pollModal = new PollModal supermodel: @supermodel, poll: @poll, userPollsRecord: @userPollsRecord
@pollModal.render()
$('#poll-view').empty().append @pollModal.el
#pollModal.afterInsert() # This blurs the active input; don't do it
@pollModal.$el.removeClass('modal fade').show()
@pollModal.on 'vote-updated', =>
@hush = true
@treema.set '/answers', @pollModal.poll.get('answers')
@hush = false
savePoll: (e) ->
@treema.endExistingEdits()
for key, value of @treema.data
@poll.set(key, value)
res = @poll.save()
res.error (collection, response, options) =>
console.error response
res.success =>
url = "/editor/poll/#{@poll.get('slug') or @poll.id}"
document.location.href = url
confirmDeletion: ->
renderData =
'confirmTitle': 'Are you really sure?'
'confirmBody': 'This will completely delete the poll, potentially breaking a lot of stuff you don\'t want breaking. Are you entirely sure?'
'confirmDecline': 'Not really'
'confirmConfirm': 'Definitely'
confirmModal = new ConfirmModal renderData
confirmModal.on 'confirm', @deletePoll
@openModalView confirmModal
deletePoll: =>
console.debug 'deleting'
$.ajax
type: 'DELETE'
success: ->
noty
timeout: 5000
text: 'Aaaand it\'s gone.'
type: 'success'
layout: 'topCenter'
_.delay ->
app.router.navigate '/editor/poll', trigger: true
, 500
error: (jqXHR, status, error) ->
console.error jqXHR
timeout: 5000
text: "Deleting poll failed with error code #{jqXHR.status}"
type: 'error'
layout: 'topCenter'
url: "/db/poll/#{@poll.id}"

View file

@ -0,0 +1,19 @@
SearchView = require 'views/common/SearchView'
module.exports = class PollSearchView extends SearchView
id: 'editor-poll-home-view'
modelLabel: 'Poll'
model: require 'models/Poll'
modelURL: '/db/poll'
tableTemplate: require 'templates/editor/poll/poll-search-table'
projection: ['name', 'description', 'slug', 'priority', 'created']
getRenderData: ->
context = super()
context.currentEditor = 'editor.poll_title'
context.currentNew = 'editor.new_poll_title'
context.currentNewSignup = 'editor.new_poll_title_login'
context.currentSearch = 'editor.poll_search_title'
context.newModelsAdminOnly = true
context.unauthorized = true unless me.isAdmin()
context

View file

@ -14,5 +14,3 @@ module.exports = class I18NEditCampaignView extends I18NEditModelView
@wrapRow 'Campaign short name', ['name'], name, i18n[lang]?.name, []
if fullName = @model.get('fullName')
@wrapRow 'Campaign full name', ['fullName'], fullName, i18n[lang]?.fullName, []
# TODO: saves to this don't work since Campaigns don't use versioning. What to do?

View file

@ -0,0 +1,21 @@
I18NEditModelView = require './I18NEditModelView'
Poll = require 'models/Poll'
module.exports = class I18NEditPollView extends I18NEditModelView
id: "i18n-edit-poll-view"
modelClass: Poll
buildTranslationList: ->
lang = @selectedLanguage
# name, description
if i18n = @model.get('i18n')
if name = @model.get('name')
@wrapRow "Poll name", ['name'], name, i18n[lang]?.name, []
if description = @model.get('description')
@wrapRow "Poll description", ['description'], description, i18n[lang]?.description, []
# answers
for answer, index in @model.get('answers') ? []
if i18n = answer.i18n
@wrapRow 'Answer', ['text'], answer.text, i18n[lang]?.text, ['answers', index]

View file

@ -7,6 +7,7 @@ ThangType = require 'models/ThangType'
Level = require 'models/Level'
Achievement = require 'models/Achievement'
Campaign = require 'models/Campaign'
Poll = require 'models/Poll'
languages = _.keys(require 'locale/locale').sort()
PAGE_SIZE = 100
@ -36,8 +37,9 @@ module.exports = class I18NHomeView extends RootView
@levels = new CocoCollection([], { url: '/db/level?view=i18n-coverage', project: project, model: Level })
@achievements = new CocoCollection([], { url: '/db/achievement?view=i18n-coverage', project: project, model: Achievement })
@campaigns = new CocoCollection([], { url: '/db/campaign?view=i18n-coverage', project: project, model: Campaign })
@polls = new CocoCollection([], { url: '/db/poll?view=i18n-coverage', project: project, model: Poll })
for c in [@thangTypes, @components, @levels, @achievements, @campaigns]
for c in [@thangTypes, @components, @levels, @achievements, @campaigns, @polls]
c.skip = 0
c.fetch({data: {skip: 0, limit: PAGE_SIZE}, cache:false})
@supermodel.loadCollection(c, 'documents')
@ -52,6 +54,7 @@ module.exports = class I18NHomeView extends RootView
when 'Achievement' then '/i18n/achievement/'
when 'Level' then '/i18n/level/'
when 'Campaign' then '/i18n/campaign/'
when 'Poll' then '/i18n/poll/'
getMore = collection.models.length is PAGE_SIZE
@aggregateModels.add(collection.models)
@render()

View file

@ -17,6 +17,9 @@ utils = require 'core/utils'
require 'vendor/three'
ParticleMan = require 'core/ParticleMan'
ShareProgressModal = require 'views/play/modal/ShareProgressModal'
UserPollsRecord = require 'models/UserPollsRecord'
Poll = require 'models/Poll'
PollModal = require 'views/play/modal/PollModal'
trackedHourOfCode = false
@ -53,6 +56,7 @@ module.exports = class CampaignView extends RootView
'mouseenter .portals': 'onMouseEnterPortals'
'mouseleave .portals': 'onMouseLeavePortals'
'mousemove .portals': 'onMouseMovePortals'
'click .poll': 'showPoll'
constructor: (options, @terrain) ->
super options
@ -95,6 +99,7 @@ module.exports = class CampaignView extends RootView
@hadEverChosenHero = me.get('heroConfig')?.thangType
@listenTo me, 'change:purchased', -> @renderSelectors('#gems-count')
@listenTo me, 'change:spent', -> @renderSelectors('#gems-count')
@listenTo me, 'change:earned', -> @renderSelectors('#gems-count')
@listenTo me, 'change:heroConfig', -> @updateHero()
window.tracker?.trackEvent 'Loaded World Map', category: 'World Map', label: @terrain
@ -216,7 +221,7 @@ module.exports = class CampaignView extends RootView
super()
@onWindowResize()
unless application.isIPadApp
_.defer => @$el?.find('.game-controls .btn').addClass('has-tooltip').tooltip() # Have to defer or i18n doesn't take effect.
_.defer => @$el?.find('.game-controls .btn:not(.poll)').addClass('has-tooltip').tooltip() # Have to defer or i18n doesn't take effect.
view = @
@$el.find('.level, .campaign-switch').addClass('has-tooltip').tooltip().each ->
return unless me.isAdmin() and view.editorMode
@ -369,6 +374,7 @@ module.exports = class CampaignView extends RootView
for session in @sessions.models
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
@render()
@loadUserPollsRecord() unless me.get 'anonymous'
onCampaignsLoaded: (e) ->
@render()
@ -560,3 +566,49 @@ module.exports = class CampaignView extends RootView
route: "/play/#{campaignSlug}"
viewClass: CampaignView
viewArgs: [{supermodel: @supermodel}, campaignSlug]
loadUserPollsRecord: ->
url = "/db/user.polls.record/-/user/#{me.id}"
@userPollsRecord = new UserPollsRecord().setURL url
onRecordSync = ->
return if @destroyed
@userPollsRecord.url = -> '/db/user.polls.record/' + @id
# TODO: only load poll if it's been a day
@loadPoll()
@listenToOnce @userPollsRecord, 'sync', onRecordSync
@userPollsRecord = @supermodel.loadModel(@userPollsRecord, 'user_polls_record', null, 0).model
onRecordSync.call @ if @userPollsRecord.loaded
loadPoll: ->
lastPollID = _.last _.keys @userPollsRecord.get 'polls'
url = "/db/poll/#{lastPollID or '-'}/next"
@poll = new Poll().setURL url
onPollSync = ->
return if @destroyed
@poll.url = -> '/db/poll/' + @id
_.delay (=> @activatePoll?()), 1000
onPollError = (poll, response, request) ->
if response.status is 404
console.log 'There are no more polls left.'
else
console.error "Couldn't load poll:", response.status, response.statusText
delete @poll
@listenToOnce @poll, 'sync', onPollSync
@listenToOnce @poll, 'error', onPollError
@poll = @supermodel.loadModel(@poll, 'poll', null, 0).model
onPollSync.call @ if @poll.loaded
activatePoll: ->
pollTitle = "#{$.i18n.t 'play.poll'}: #{utils.i18n @poll.attributes, 'name'}"
$pollButton = @$el.find('button.poll').removeClass('hidden').addClass('highlighted').attr(title: pollTitle).addClass('has-tooltip').tooltip title: pollTitle
if me.get('lastLevel') is 'shadow-guard'
@showPoll()
else
$pollButton.tooltip 'show'
showPoll: ->
pollModal = new PollModal supermodel: @supermodel, poll: @poll, userPollsRecord: @userPollsRecord
@openModalView pollModal
$pollButton = @$el.find 'button.poll'
pollModal.on 'vote-updated', ->
$pollButton.removeClass('highlighted').tooltip 'hide'

View file

@ -148,7 +148,6 @@ module.exports = class Spell
cb(aether.hasChangedSignificantly((newSource ? @originalSource), (currentSource ? @source), true, true))
createAether: (thang) ->
aceConfig = me.get('aceConfig') ? {}
writable = @permissions.readwrite.length > 0
skipProtectAPI = @skipProtectAPI or not writable
problemContext = @createProblemContext thang

View file

@ -0,0 +1,132 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/play/modal/poll-modal'
utils = require 'core/utils'
UserPollsRecord = require 'models/UserPollsRecord'
module.exports = class PollModal extends ModalView
id: 'poll-modal'
template: template
subscriptions: {}
events:
'click #close-modal': 'hide'
'click .answer:not(.selected)': 'onClickAnswer'
constructor: (options) ->
super options
@poll = options.poll
@userPollsRecord = options.userPollsRecord
@previousAnswer = (@userPollsRecord.get('polls') ? {})[@poll.id]
@previousReward = (@userPollsRecord.get('rewards') ? {})[@poll.id]
getRenderData: (c) ->
c = super c
c.poll = @poll
c.i18n = utils.i18n
c.marked = marked
c
afterRender: ->
super()
@playSound 'game-menu-open'
@updateAnswers()
onHidden: ->
super()
@playSound 'game-menu-close'
updateAnswers: (answered) ->
myAnswer = (@userPollsRecord.get('polls') ? {})[@poll.id]
answered = myAnswer?
@$el.find('table, .random-gems-container-wrapper').toggleClass 'answered', answered
return unless answered
@awardRandomGems()
# Count total votes and find the answer with the most votes.
[maxVotes, totalVotes] = [0, 0]
for answer in @poll.get('answers') or []
votes = answer.votes or 0
--votes if answer.key is @previousAnswer
++votes if answer.key is myAnswer
answer.votes = votes
totalVotes += votes
maxVotes = Math.max maxVotes, votes or 0
@previousAnswer = myAnswer
@poll.set 'answers', @poll.get('answers') # Update vote count locally (won't save to server).
# Update each answer cell according to its share of max and total votes.
for answer in @poll.get 'answers'
$answer = @$el.find(".answer[data-answer='#{answer.key}']")
$answer.toggleClass 'selected', answer.key is myAnswer
votes = answer.votes or 0
votes = maxVotes = totalVotes = 1 unless totalVotes # If no votes yet, just pretend we voted for the first one.
widthPercentage = (100 * votes / maxVotes) + '%'
votePercentage = Math.round(100 * votes / totalVotes) + '%'
$answer.find('.progress-bar').css('width', '0%').animate({width: widthPercentage}, 'slow')
$answer.find('.vote-percentage').text votePercentage
@trigger 'vote-updated'
onClickAnswer: (e) ->
$selectedAnswer = $(e.target).closest('.answer')
pollVotes = @userPollsRecord.get('polls') ? {}
pollVotes[@poll.id] = $selectedAnswer.data('answer')
@userPollsRecord.set 'polls', pollVotes
@updateAnswers true
@userPollsRecord.save {polls: pollVotes}, {success: => @awardRandomGems?()}
awardRandomGems: ->
return unless reward = (@userPollsRecord.get('rewards') ? {})[@poll.id]
@$randomNumber = @$el.find('#random-number-comment').empty()
@$randomGems = @$el.find('#random-gems-comment').hide()
@$totalGems = @$el.find('#total-gems-comment').hide()
commentStart = commentStarts[me.get('aceConfig')?.language ? 'python']
randomNumber = reward.random
randomGems = Math.ceil 2 * randomNumber * reward.level
totalGems = if @previousReward then me.gems() else Math.round me.gems() + randomGems
if @previousReward
utils.replaceText @$randomNumber.show(), commentStart + randomNumber.toFixed(7)
utils.replaceText @$randomGems.show(), commentStart + randomGems
utils.replaceText @$totalGems.show(), commentStart + totalGems
else
gemNoisesPlayed = 0
for i in [0 .. 1000] by 25
do (i) =>
@$randomNumber.queue ->
number = if i is 1000 then randomNumber else Math.random()
utils.replaceText $(@), commentStart + number.toFixed(7)
$(@).dequeue()
if Math.random() < randomGems / 40
gemTrigger = 'gem-' + (gemNoisesPlayed % 4) # 4 gem sounds
++gemNoisesPlayed
Backbone.Mediator.publish 'audio-player:play-sound', trigger: gemTrigger, volume: 0.475 + i / 2000
@$randomNumber.delay 25
@$randomGems.delay(1100).queue ->
utils.replaceText $(@), commentStart + randomGems
$(@).show()
$(@).dequeue()
@$totalGems.delay(1200).queue ->
utils.replaceText $(@), commentStart + totalGems
$(@).show()
$(@).dequeue()
@previousReward = reward
_.delay (=>
return if @destroyed
earned = me.get('earned') ? {}
earned.gems += randomGems
me.set 'earned', earned
me.trigger 'change:earned'
), 1200
commentStarts =
javascript: '// '
python: '# '
coffeescript: '# '
clojure: '; '
lua: '-- '
io: '// '

View file

@ -32,6 +32,7 @@ ArticleSchema.index({slug: 1}, {name: 'slug index', sparse: true, unique: true})
ArticleSchema.plugin(plugins.NamedPlugin)
ArticleSchema.plugin(plugins.VersionedPlugin)
ArticleSchema.plugin(plugins.SearchablePlugin, {searchable: ['body', 'name']})
ArticleSchema.plugin(plugins.TranslationCoveragePlugin)
ArticleSchema.plugin(plugins.PatchablePlugin)
module.exports = mongoose.model('article', ArticleSchema)

View file

@ -21,6 +21,8 @@ module.exports.handlers =
'mail_sent': 'mail/sent/mail_sent_handler'
'achievement': 'achievements/achievement_handler'
'earned_achievement': 'achievements/earned_achievement_handler'
'poll': 'polls/poll_handler'
'user_polls_record': 'polls/user_polls_record_handler'
module.exports.routes =
[

View file

@ -30,8 +30,6 @@ LevelSessionSchema.index({submitted: 1, team: 1, levelID: 1, submitDate: -1}, {n
LevelSessionSchema.plugin(plugins.PermissionsPlugin)
LevelSessionSchema.plugin(AchievablePlugin)
previous = {}
LevelSessionSchema.post 'init', (doc) ->
unless doc.previousStateInfo
doc.previousStateInfo =

34
server/polls/Poll.coffee Normal file
View file

@ -0,0 +1,34 @@
mongoose = require 'mongoose'
plugins = require '../plugins/plugins'
jsonSchema = require '../../app/schemas/models/poll.schema'
log = require 'winston'
PollSchema = new mongoose.Schema {
created:
type: Date
'default': Date.now
}, {strict: false, minimize: false}
PollSchema.index {priority: 1}
# Just duplicating indexes that get created here by plugins for completeness
PollSchema.index {i18nCoverage: 1}, {name: 'translation coverage index', sparse: true}
PollSchema.index {slug: 1}, {name: 'slug index', sparse: true, unique: true}
PollSchema.plugin plugins.NamedPlugin
PollSchema.plugin plugins.PatchablePlugin
PollSchema.plugin plugins.TranslationCoveragePlugin
PollSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'description']}
PollSchema.statics.privateProperties = []
PollSchema.statics.editableProperties = [
'name'
'description'
'answers'
'i18n'
'i18nCoverage'
'priority'
]
PollSchema.statics.jsonSchema = jsonSchema
module.exports = Poll = mongoose.model 'poll', PollSchema, 'polls'

View file

@ -0,0 +1,51 @@
mongoose = require 'mongoose'
plugins = require '../plugins/plugins'
jsonSchema = require '../../app/schemas/models/user-polls-record.schema'
log = require 'winston'
Poll = require './Poll'
User = require '../users/User'
UserPollsRecordSchema = new mongoose.Schema {}, {strict: false, minimize: false}
UserPollsRecordSchema.index {user: 1}, {unique: true, name: 'user polls record index'}
UserPollsRecordSchema.post 'init', (doc) ->
doc.previousPolls ?= _.clone doc.get('polls') ? {}
UserPollsRecordSchema.pre 'save', (next) ->
return next() unless @previousPolls?
@set 'changed', new Date()
rewards = @get('rewards') ? {}
level = @get('level') ? {}
gemDelta = 0
for pollID, answer of @get('polls') ? {}
previousAnswer = @previousPolls[pollID]
updatePollVotes pollID, answer, previousAnswer unless answer is previousAnswer
unless rewards[pollID]
rewards[pollID] = reward = random: Math.random(), level: level
gemDelta += Math.ceil 2 * reward.random * reward.level
@set 'rewards', rewards
@markModified 'rewards'
updateUserGems @get('user'), gemDelta if gemDelta
next()
updatePollVotes = (pollID, answer, previousAnswer) ->
Poll.findById mongoose.Types.ObjectId(pollID), {}, (err, poll) ->
return log.error err if err
answers = poll.get 'answers'
_.find(answers, key: answer)?.votes++
_.find(answers, key: previousAnswer)?.votes-- if previousAnswer
poll.set 'answers', answers
poll.markModified 'answers'
poll.save (err, newPoll, numberAffected) ->
return log.error err if err
updateUserGems = (userID, gemDelta) ->
User.update {_id: mongoose.Types.ObjectId(userID)}, {$inc: {'earned.gems': gemDelta}}, (err, numberAffected) ->
return log.error err if err
UserPollsRecordSchema.statics.privateProperties = []
UserPollsRecordSchema.statics.editableProperties = ['polls']
UserPollsRecordSchema.statics.jsonSchema = jsonSchema
module.exports = UserPollsRecord = mongoose.model 'user.polls.record', UserPollsRecordSchema, 'user.polls.records'

View file

@ -0,0 +1,51 @@
Poll = require './Poll'
Handler = require '../commons/Handler'
async = require 'async'
mongoose = require 'mongoose'
PollHandler = class PollHandler extends Handler
modelClass: Poll
jsonSchema: require '../../app/schemas/models/poll.schema'
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
hasAccess: (req) ->
req.method in ['GET', 'PUT'] or req.user?.isAdmin()
hasAccessToDocument: (req, document, method=null) ->
method = (method or req.method).toLowerCase()
return true if req.user?.isAdmin()
return true if method is 'get'
return true if method in ['post', 'put'] and @isJustFillingTranslations req, document
false
getByRelationship: (req, res, args...) ->
relationship = args[1]
return @getNextPoll(req, res, args[0]) if relationship is 'next'
super arguments...
getNextPoll: (req, res, lastPollID) ->
@findPollPriority lastPollID, (err, lastPriority) =>
return @sendDatabaseError(res, err) if err
@getNextPollAfterPriority lastPriority, (err, poll) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless poll
@sendSuccess res, @formatEntity(req, poll)
findPollPriority: (lastPollID, callback) ->
return callback null, -9001 #unless lastPollID
Poll.findById mongoose.Types.ObjectId(lastPollID), 'priority', {lean: true}, (err, poll) ->
callback err, poll?.priority
getNextPollAfterPriority: (priority, callback) ->
Poll.findOne({priority: {$gt: priority}}).sort('priority').exec callback
delete: (req, res, slugOrID) ->
return @sendForbiddenError res unless req.user?.isAdmin()
@getDocumentForIdOrSlug slugOrID, (err, document) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless document?
document.remove (err, document) =>
return @sendDatabaseError(res, err) if err
@sendNoContent res
module.exports = new PollHandler()

View file

@ -0,0 +1,39 @@
UserPollsRecord = require './UserPollsRecord'
Handler = require '../commons/Handler'
async = require 'async'
mongoose = require 'mongoose'
UserPollsRecordHandler = class UserPollsRecordHandler extends Handler
modelClass: UserPollsRecord
jsonSchema: require '../../app/schemas/models/user-polls-record.schema'
hasAccess: (req) ->
req.user and (req.method in ['GET', 'POST', 'PUT'] or req.user?.isAdmin())
hasAccessToDocument: (req, document, method=null) ->
req.user?.isAdmin() or req.user?._id.equals document._id
getByRelationship: (req, res, args...) ->
relationship = args[1]
return @getUserPollsRecord(req, res, args[2]) if relationship is 'user'
super arguments...
getUserPollsRecord: (req, res, userID) ->
UserPollsRecord.findOne(user: userID).exec (err, doc) =>
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, doc) if doc?
@createAndSaveNewUserPollsRecord userID, req, res
createAndSaveNewUserPollsRecord: (userID, req, res) =>
initVals = user: userID, polls: {}, level: req.user.level()
userPollsRecord = new UserPollsRecord initVals
userPollsRecord.save (err) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, userPollsRecord))
saveChangesToDocument: (req, document, done) ->
document.set 'level', req.user.level()
super req, document, done
module.exports = new UserPollsRecordHandler()

View file

@ -217,6 +217,12 @@ UserSchema.methods.isPremium = ->
return true if _.isString(stripeObject.free) and new Date() < new Date(stripeObject.free)
return false
UserSchema.methods.level = ->
xp = @get('points') or 0
a = 5
b = c = 100
if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + c))) + 1 else 1
UserSchema.statics.saveActiveUser = (id, event, done=null) ->
# TODO: Disabling this until we know why our app servers CPU grows out of control.
return done?()