mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Polls!
This commit is contained in:
parent
6954175fa8
commit
e21360127d
35 changed files with 931 additions and 18 deletions
BIN
app/assets/images/pages/play/modal/random-gems-background.png
Normal file
BIN
app/assets/images/pages/play/modal/random-gems-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
8
app/models/Poll.coffee
Normal 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
|
7
app/models/UserPollsRecord.coffee
Normal file
7
app/models/UserPollsRecord.coffee
Normal 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'
|
|
@ -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
|
||||
|
|
23
app/schemas/models/poll.schema.coffee
Normal file
23
app/schemas/models/poll.schema.coffee
Normal 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
|
20
app/schemas/models/user-polls-record.schema.coffee
Normal file
20
app/schemas/models/user-polls-record.schema.coffee
Normal 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
|
|
@ -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
|
||||
|
|
17
app/styles/editor/poll/poll-edit-view.sass
Normal file
17
app/styles/editor/poll/poll-edit-view.sass
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
190
app/styles/play/modal/poll-modal.sass
Normal file
190
app/styles/play/modal/poll-modal.sass
Normal 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
|
|
@ -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}")
|
||||
|
|
31
app/templates/editor/poll/poll-edit-view.jade
Normal file
31
app/templates/editor/poll/poll-edit-view.jade
Normal 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.
|
19
app/templates/editor/poll/poll-search-table.jade
Normal file
19
app/templates/editor/poll/poll-search-table.jade
Normal 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()
|
|
@ -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
|
||||
|
|
40
app/templates/play/modal/poll-modal.jade
Normal file
40
app/templates/play/modal/poll-modal.jade
Normal 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
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
132
app/views/editor/poll/PollEditView.coffee
Normal file
132
app/views/editor/poll/PollEditView.coffee
Normal 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}"
|
19
app/views/editor/poll/PollSearchView.coffee
Normal file
19
app/views/editor/poll/PollSearchView.coffee
Normal 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
|
|
@ -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?
|
||||
|
|
21
app/views/i18n/I18NEditPollView.coffee
Normal file
21
app/views/i18n/I18NEditPollView.coffee
Normal 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]
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
132
app/views/play/modal/PollModal.coffee
Normal file
132
app/views/play/modal/PollModal.coffee
Normal 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: '// '
|
|
@ -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)
|
||||
|
|
|
@ -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 =
|
||||
[
|
||||
|
|
|
@ -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
34
server/polls/Poll.coffee
Normal 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'
|
51
server/polls/UserPollsRecord.coffee
Normal file
51
server/polls/UserPollsRecord.coffee
Normal 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'
|
51
server/polls/poll_handler.coffee
Normal file
51
server/polls/poll_handler.coffee
Normal 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()
|
39
server/polls/user_polls_record_handler.coffee
Normal file
39
server/polls/user_polls_record_handler.coffee
Normal 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()
|
|
@ -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?()
|
||||
|
|
Loading…
Reference in a new issue