Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-11-25 16:54:36 -08:00
commit 98d8a2cfb3
43 changed files with 587 additions and 805 deletions

View file

@ -21,10 +21,10 @@ module.exports = class CocoRouter extends Backbone.Router
'about': go('AboutView') 'about': go('AboutView')
'account': go('account/MainAccountView') 'account': go('account/MainAccountView')
'account/settings': go('account/AccountSettingsView') 'account/settings': go('account/AccountSettingsRootView')
'account/unsubscribe': go('account/UnsubscribeView') 'account/unsubscribe': go('account/UnsubscribeView')
'account/profile': go('user/JobProfileView') # legacy URL, sent in emails 'account/profile': go('user/JobProfileView') # legacy URL, sent in emails
#'account/payment' 'account/payments': go('account/PaymentsView')
'admin': go('admin/MainAdminView') 'admin': go('admin/MainAdminView')
'admin/candidates': go('admin/CandidatesView') 'admin/candidates': go('admin/CandidatesView')
@ -99,9 +99,7 @@ module.exports = class CocoRouter extends Backbone.Router
'test(/*subpath)': go('TestView') 'test(/*subpath)': go('TestView')
'user/:slugOrID': go('user/MainUserView') 'user/:slugOrID': go('user/MainUserView')
'user/:slugOrID/stats': go('user/AchievementsView')
'user/:slugOrID/profile': go('user/JobProfileView') 'user/:slugOrID/profile': go('user/JobProfileView')
#'user/:slugOrID/code': go('user/CodeView')
'*name': 'showNotFoundView' '*name': 'showNotFoundView'

View file

@ -6,10 +6,13 @@ module.exports = class ThangNamesCollection extends CocoCollection
model: ThangType model: ThangType
isCachable: false isCachable: false
constructor: (@ids) -> super() constructor: (@ids) ->
super()
@ids.sort()
if @ids.length > 55
console.error 'Too many ids, we\'ll likely go over the GET url kind-of-limit of 2000 characters.'
fetch: (options) -> fetch: (options) ->
options ?= {} options ?= {}
method = if application.isIPadApp then 'GET' else 'POST' # Not sure why this was required that one time. _.extend options, {data: {ids: @ids}}
_.extend options, {type: method, data: {ids: @ids}}
super(options) super(options)

View file

@ -210,7 +210,7 @@ module.exports = LevelOptions =
hidesRealTimePlayback: true hidesRealTimePlayback: true
hidesCodeToolbar: true hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'} restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
'village-guard': 'village-guard':
hidesCodeToolbar: true hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'} requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
@ -244,7 +244,7 @@ module.exports = LevelOptions =
# Ranger branch # Ranger branch
'munchkin-harvest': 'munchkin-harvest':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {} restrictedGear: {}
'swift-dagger': 'swift-dagger':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'} requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'}
@ -255,7 +255,7 @@ module.exports = LevelOptions =
# Wizard branch # Wizard branch
'arcane-ally': 'arcane-ally':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'} requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {eyes: 'crude-glasses'} restrictedGear: {eyes: 'crude-glasses'}
'touch-of-death': 'touch-of-death':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'} requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'}

View file

@ -791,6 +791,13 @@
account: account:
recently_played: "Recently Played" recently_played: "Recently Played"
no_recent_games: "No games played during the past two weeks." no_recent_games: "No games played during the past two weeks."
payments: "Payments"
service_apple: "Apple"
service_web: "Web"
paid_on: "Paid On"
service: "Service"
price: "Price"
gems: "Gems"
loading_error: loading_error:
could_not_load: "Error loading from server" could_not_load: "Error loading from server"

View file

@ -0,0 +1,5 @@
CocoModel = require('./CocoModel')
module.exports = class Payment extends CocoModel
@className: "Payment"
urlRoot: "/db/payment"

View file

@ -232,7 +232,9 @@ module.exports = class ThangType extends CocoModel
stage?.toDataURL() stage?.toDataURL()
getPortraitStage: (spriteOptionsOrKey, size=100) -> getPortraitStage: (spriteOptionsOrKey, size=100) ->
return unless @isFullyLoaded() canvas = $("<canvas width='#{size}' height='#{size}'></canvas>")
stage = new createjs.Stage(canvas[0])
return stage unless @isFullyLoaded()
key = spriteOptionsOrKey key = spriteOptionsOrKey
key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key)) key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key))
spriteSheet = @spriteSheets[key] spriteSheet = @spriteSheets[key]
@ -242,8 +244,6 @@ module.exports = class ThangType extends CocoModel
spriteSheet = @buildSpriteSheet(options) spriteSheet = @buildSpriteSheet(options)
return if _.isString spriteSheet return if _.isString spriteSheet
return unless spriteSheet return unless spriteSheet
canvas = $("<canvas width='#{size}' height='#{size}'></canvas>")
stage = new createjs.Stage(canvas[0])
sprite = new createjs.Sprite(spriteSheet) sprite = new createjs.Sprite(spriteSheet)
pt = @actions.portrait?.positions?.registration pt = @actions.portrait?.positions?.registration
sprite.regX = pt?.x or 0 sprite.regX = pt?.x or 0

View file

@ -0,0 +1,49 @@
#account-settings-root-view
//- Fixed save button
#site-content-area
padding-bottom: 44px
#save-button-container
position: fixed
bottom: 0
left: 0
right: 0
z-index: 10
background: gray
padding: 5px
#save-button
width: 100%
&.btn-info, &.btn-danger
opacity: 1.0
#account-settings-view
.row
padding-top: 20px
//- Panels
.panel-heading
font-family: Open Sans Condensed
font-weight: bold
.panel-title
font-size: 20px
//- Panel specific stuff
.profile-photo
max-width: 100%
max-height: 200px
display: block
margin-bottom: 10px
#email-panel
#specific-notification-settings
padding-left: 20px
margin-left: 20px
border-left: 1px solid gray

View file

@ -1,32 +0,0 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
#account-home
dl
margin-bottom: 0px
img#picture
max-width: 100%
.panel
margin-bottom: 10px
h2
margin-bottom: 0px
a
font-size: 28px
margin-left: 5px
.panel-title > a
margin-left: 5px
color: rgb(11, 99, 188)
.panel-me
td
padding-left: 15px
.panel-emails
h4
font-family: $font-family-base

View file

@ -0,0 +1,9 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
#main-account-view
#account-links
width: 300px
#account-links .btn
width: 100%

View file

@ -1,91 +0,0 @@
#account-settings-view
.nav
margin-bottom: 10px
.tab-content
border: 1px solid #aaa
padding: 20px
background: #eee
border-radius: 5px
#save-button-container
position: fixed
top: 100px
width: 1000px
z-index: 10
#save-button
float: right
&.btn-info, &.btn-danger
opacity: 1.0
.gravatar-fallback
margin-top: 10px
input.range
position: relative
top: 4px
div.range-color
position: relative
top: 6px
height: 16px
width: 16px
display: inline-block
margin-left: 10px
.help-inline
position: relative
top: 3px
left: 10px
font-size: 12px
.form
max-width: 600px
#email-pane
#specific-notification-settings
padding-left: 20px
margin-left: 20px
border-left: 1px solid gray
#job-profile-view
.profile-preview-button
&.bottom-preview
margin: 15px 0 0 0
.sample-profile-thumbnail
margin-top: -60px
.profile-completion-progress
width: 100%
display: inline-block
height: 33px
.progress-bar
line-height: 33px
.progress-next-item
margin-top: -20px
margin-bottom: 15px
#job-profile-treema
background-color: white
input
width: 790px
.treema-description
font-size: 14px
line-height: 22px
opacity: 1
.treema-row
padding-top: 6px
.treema-image-file
img
display: block
clear: both
max-width: 300px

View file

@ -66,37 +66,6 @@ $user-achievements-scale: 0.8
overflow: hidden overflow: hidden
text-overflow: ellipsis text-overflow: ellipsis
// Specific to the user stats page
#user-achievements-view
.achievement-body
width: 335px
height: 120px
margin: 10px 0px
.achievement-icon
width: $overall-scale * $icon-size * $user-achievements-scale
height: $overall-scale * $icon-size * $user-achievements-scale
top: -5px
.achievement-image
img
width: $overall-scale * $user-achievements-scale * $icon-image-size
.achievement-content
margin-left: 60px
margin-right: 5px
width: 260px
height: 100px
padding: 15px 10px 20px 60px
.achievement-title
font-size: 20px
.achievement-description
font-size: 12px
line-height: 1.3em
max-height: 2.6em
.achievement-popup .achievement-popup
padding: $overall-scale * 20px 0px padding: $overall-scale * 20px 0px
position: relative position: relative

View file

@ -75,6 +75,7 @@ a
.progress .progress
width: 50% width: 50%
margin: 0 25% margin: 0 25%
margin-bottom: 20px
// all loading screens // all loading screens
.loading-container .loading-container

View file

@ -6,10 +6,17 @@
&.show-background &.show-background
background: url(/images/pages/base/background.jpg) top center no-repeat background: url(/images/pages/base/background.jpg) top center no-repeat
background-color: rgb(150,202,68) background-color: rgb(150,202,68)
@media screen and ( max-height: 800px )
background-position: center -226px
padding-top: 185px padding-top: 185px
max-width: 1920px max-width: 1920px
margin: 0 auto margin: 0 auto
@media screen and ( max-height: 800px )
padding-top: 50px
//- Nav //- Nav
#site-nav #site-nav
@ -22,7 +29,10 @@
text-align: center text-align: center
min-width: 1024px min-width: 1024px
z-index: 1 z-index: 1
@media screen and ( max-height: 800px )
top: -80px
#nav-logo #nav-logo
position: absolute position: absolute
margin-right: auto margin-right: auto
@ -31,22 +41,31 @@
right: 0 right: 0
top: -45px top: -45px
@media screen and ( max-height: 800px )
display: none
#small-nav-logo
display: none
@media screen and ( max-height: 800px )
display: inline-block
height: 30px
#site-nav-links #site-nav-links
position: absolute position: absolute
bottom: 21px bottom: 21px
left: 0 left: 0
right: 0 right: 0
a & > a
color: rgb(158,135,119) color: rgb(158,135,119)
&:hover &:hover
color: $white color: $white
a, button, select & > a, button, select
font-size: 18px font-size: 18px
text-transform: uppercase text-transform: uppercase
font-family: Open Sans Condensed font-family: Open Sans Condensed
margin: 0 7px margin: 0 7px
button, select button, select
position: relative position: relative
@ -81,57 +100,44 @@
width: 18px width: 18px
.dropdown-menu .dropdown-menu
//left: auto // this busts it, not sure why it's in width: 180px
width: 280px
padding: 0px padding: 0px
border-radius: 0px border-radius: 0px
font-family: Open Sans Condensed
font-variant: small-caps
> .user-dropdown-header .user-dropdown-header
position: relative
background: #E4CF8C background: #E4CF8C
height: 160px height: 160px
padding: 10px padding: 10px
text-align: center text-align: center
color: black color: black
border-bottom: #32281e 1px solid border-bottom: #32281e 1px solid
> a:hover
background-color: transparent
img img
border: #e3be7a 8px solid border: #e3be7a 8px solid
height: 98px // Includes the border height: 98px // Includes the border
&:hover &:hover
box-shadow: 0 0 20px #e3be7a box-shadow: 0 0 20px #e3be7a
> h3
h3
font-variant: small-caps
font-family: Open Sans Condensed
margin-top: 10px margin-top: 10px
text-shadow: 2px 2px 3px white text-shadow: 2px 2px 3px white
color: #31281E color: #31281E
.user-level .user-level
position: absolute position: absolute
top: 73px top: 73px
right: 86px right: 40px
color: gold color: gold
text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black
.user-dropdown-body li
color: black color: black
padding: 15px font-size: 16px
letter-spacing: 1px
font: 15px 'Helvetica Neue', Helvetica, Arial, sans-serif
+clearfix()
.user-dropdown-footer
padding: 10px
margin-left: 0px
font-size: 14px
+clearfix()
.btn-flat
border: #ddd 1px solid
border-radius: 0px
margin: 0px
#logout-button
font-weight: bold
//- Content //- Content
@ -141,6 +147,7 @@
width: 1024px width: 1024px
border: 5px solid rgb(110,88,41) border: 5px solid rgb(110,88,41)
padding: 20px 12px padding: 20px 12px
min-height: 300px
//- Footer //- Footer

View file

@ -4,8 +4,9 @@
#home-view #home-view
#spacer #spacer
//height: 750px // No one could see this; let's shrink it as much as we can. height: 626px
height: 606px @media screen and ( max-height: 800px )
height: 510px
#play-button, #or-ipad, #apple-store-button, #slogan, .alert #play-button, #or-ipad, #apple-store-button, #slogan, .alert
text-align: center text-align: center
@ -27,6 +28,9 @@
top: 308px top: 308px
width: 218px width: 218px
height: 219px height: 219px
@media screen and ( max-height: 800px )
top: 78px
background-image: url(/images/pages/home/play_button.png) background-image: url(/images/pages/home/play_button.png)
background-position: 0 219px background-position: 0 219px
@ -40,11 +44,17 @@
color: rgb(119,101,84) color: rgb(119,101,84)
font-size: 17px font-size: 17px
max-width: 211px max-width: 211px
@media screen and ( max-height: 800px )
top: 310px
#apple-store-button #apple-store-button
top: 593px top: 593px
height: 63px height: 63px
@media screen and ( max-height: 800px )
top: 363px
#slogan #slogan
top: 681px top: 681px
height: 132px height: 132px
@ -53,6 +63,9 @@
font-size: 28px font-size: 28px
line-height: 32px line-height: 32px
color: rgb(50,40,31) color: rgb(50,40,31)
@media screen and ( max-height: 800px )
top: 451px
.alert .alert
top: 213px top: 213px

View file

@ -1,4 +1,13 @@
#play-account-modal #play-account-modal
.account-view .modal-dialog
color: black min-width: 90%
.modal-header
margin-bottom: 20px
.modal-body
max-height: 500px
overflow: scroll
border-width: 2px 0
border-color: black
border-style: solid

View file

@ -46,6 +46,9 @@
color: #555555 color: #555555
font-size: 15px font-size: 15px
margin-left: 5px margin-left: 5px
.panel-footer
text-align: right
.contributor-categories .contributor-categories
list-style: none list-style: none

View file

@ -0,0 +1,19 @@
extends /templates/base
block content
ol.breadcrumb
li
a(href="/")
span.glyphicon.glyphicon-home
li
a(href="/account", data-i18n="nav.account")
li.active(data-i18n="account_settings.title")
if !me.get('anonymous', true)
#save-button-container
button#save-button.btn-lg.btn.disabled(data-i18n="general.save" disabled="true") No Changes
#account-settings-view
block footer

View file

@ -1,34 +1,13 @@
extends /templates/base if me.get('anonymous')
.alert.alert-danger(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings.
block content else
.row
h2(data-i18n="account_settings.title") Account Settings .col-md-6
.panel.panel-default
if me.get('anonymous') .panel-heading
p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings. .panel-title(data-i18n="account_settings.me_tab")
.panel-body
else
#save-button-container
button.btn#save-button.disabled(data-i18n="general.save" disabled="true") No Changes
ul.nav.nav-pills#settings-tabs
li
a(href="#general-pane", data-toggle="tab", data-i18n="account_settings.me_tab") Me
li
a(href="#picture-pane", data-toggle="tab", data-i18n="account_settings.picture_tab") Picture
li
a(href="#wizard-pane", data-toggle="tab", data-i18n="account_settings.wizard_tab") Wizard
li
a(href="#password-pane", data-toggle="tab", data-i18n="account_settings.password_tab") Password
li
a(href="#email-pane", data-toggle="tab", data-i18n="account_settings.emails_tab") Emails
if showsJobProfileTab
li
a(href="#job-profile-pane", data-toggle="tab", data-i18n="account_settings.job_profile_tab") Job Profile
.tab-content#settings-panes
#general-pane.tab-pane
p
.form .form
- var name = me.get('name') || ''; - var name = me.get('name') || '';
- var email = me.get('email'); - var email = me.get('email');
@ -43,19 +22,20 @@ block content
.form-group.checkbox .form-group.checkbox
label(for="admin", data-i18n="account_settings.admin") Admin label(for="admin", data-i18n="account_settings.admin") Admin
input#admin(name="admin", type="checkbox", checked=admin) input#admin(name="admin", type="checkbox", checked=admin)
#picture-pane.tab-pane .panel.panel-default
h3(data-i18n="account_settings.upload_picture") Upload a picture .panel-heading
#picture-treema .panel-title(data-i18n="account_settings.picture_tab")
.gravatar-fallback .panel-body
img(src=me.getPhotoURL(256), alt="Gravatar", title="Gravatar fallback image") img.profile-photo(src=me.getPhotoURL(230), draggable="false")
input#photoURL(type="hidden", value=me.get('photoURL')||'')
#wizard-pane.tab-pane button#upload-photo-button.btn.form-control.btn-primary(data-i18n="account_settings.upload_picture")
#wizard-settings-view
.panel.panel-default
#password-pane.tab-pane .panel-heading
p .panel-title(data-i18n="account_settings.password_tab")
.panel-body
.form .form
.form-group .form-group
label.control-label(for="password", data-i18n="account_settings.new_password") New Password label.control-label(for="password", data-i18n="account_settings.new_password") New Password
@ -63,11 +43,14 @@ block content
.form-group .form-group
label.control-label(for="password2", data-i18n="account_settings.new_password_verify") Verify label.control-label(for="password2", data-i18n="account_settings.new_password_verify") Verify
input#password2.form-control(name="password2", type="password") input#password2.form-control(name="password2", type="password")
#email-pane.tab-pane
h3(data-i18n="account_settings.email_subscriptions") Email Subscriptions
p .col-md-6
#email-panel.panel.panel-default
.panel-heading
.panel-title(data-i18n="account_settings.emails_tab")
.panel-body
.form .form
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_generalNews", data-i18n="account_settings.email_announcements") Announcements label.control-label(for="email_generalNews", data-i18n="account_settings.email_announcements") Announcements
@ -85,19 +68,19 @@ block content
span.help-block(data-i18n="account_settings.email_any_notes_description") Disable to stop all activity notification emails. span.help-block(data-i18n="account_settings.email_any_notes_description") Disable to stop all activity notification emails.
fieldset#specific-notification-settings fieldset#specific-notification-settings
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_recruitNotes", data-i18n="account_settings.email_recruit_notes") Job Opportunities label.control-label(for="email_recruitNotes", data-i18n="account_settings.email_recruit_notes") Job Opportunities
input#email_recruitNotes(name="email_recruitNotes", type="checkbox", checked=subs.recruitNotes) input#email_recruitNotes(name="email_recruitNotes", type="checkbox", checked=subs.recruitNotes)
span.help-block(data-i18n="account_settings.email_recruit_notes_description") If you play really well, we may contact you about getting you a (better) job. span.help-block(data-i18n="account_settings.email_recruit_notes_description") If you play really well, we may contact you about getting you a (better) job.
hr hr
h4(data-i18n="account_settings.contributor_emails") Contributor Class Emails h4(data-i18n="account_settings.contributor_emails") Contributor Class Emails
span(data-i18n="account_settings.contribute_prefix") We're looking for people to join our party! Check out the span(data-i18n="account_settings.contribute_prefix") We're looking for people to join our party! Check out the
a(href="/contribute", data-i18n="account_settings.contribute_page") contribute page a(href="/contribute", data-i18n="account_settings.contribute_page") contribute page
span(data-i18n="account_settings.contribute_suffix") to find out more. span(data-i18n="account_settings.contribute_suffix") to find out more.
.form .form
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_archmageNews") label.control-label(for="email_archmageNews")
@ -108,7 +91,7 @@ block content
| (Coder) | (Coder)
input#email_archmageNews(name="email_archmageNews", type="checkbox", checked=subs.archmageNews) input#email_archmageNews(name="email_archmageNews", type="checkbox", checked=subs.archmageNews)
span(data-i18n="contribute.archmage_subscribe_desc").help-block Get emails about general news and announcements about CodeCombat. span(data-i18n="contribute.archmage_subscribe_desc").help-block Get emails about general news and announcements about CodeCombat.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_artisanNews") label.control-label(for="email_artisanNews")
span(data-i18n="classes.artisan_title") span(data-i18n="classes.artisan_title")
@ -118,7 +101,7 @@ block content
| (Level Builder) | (Level Builder)
input#email_artisanNews(name="email_artisanNews", type="checkbox", checked=subs.artisanNews) input#email_artisanNews(name="email_artisanNews", type="checkbox", checked=subs.artisanNews)
span(data-i18n="contribute.artisan_subscribe_desc").help-block Get emails on level editor updates and announcements. span(data-i18n="contribute.artisan_subscribe_desc").help-block Get emails on level editor updates and announcements.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_adventurerNews") label.control-label(for="email_adventurerNews")
span(data-i18n="classes.adventurer_title") span(data-i18n="classes.adventurer_title")
@ -128,7 +111,7 @@ block content
| (Level Playtester) | (Level Playtester)
input#email_adventurerNews(name="email_adventurerNews", type="checkbox", checked=subs.adventurerNews) input#email_adventurerNews(name="email_adventurerNews", type="checkbox", checked=subs.adventurerNews)
span(data-i18n="contribute.adventurer_subscribe_desc").help-block Get emails when there are new levels to test. span(data-i18n="contribute.adventurer_subscribe_desc").help-block Get emails when there are new levels to test.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_scribeNews") label.control-label(for="email_scribeNews")
span(data-i18n="classes.scribe_title") span(data-i18n="classes.scribe_title")
@ -138,7 +121,7 @@ block content
| (Article Editor) | (Article Editor)
input#email_scribeNews(name="email_scribeNews", type="checkbox", checked=subs.scribeNews) input#email_scribeNews(name="email_scribeNews", type="checkbox", checked=subs.scribeNews)
span(data-i18n="contribute.scribe_subscribe_desc").help-block Get emails about article writing announcements. span(data-i18n="contribute.scribe_subscribe_desc").help-block Get emails about article writing announcements.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_diplomatNews") label.control-label(for="email_diplomatNews")
span(data-i18n="classes.diplomat_title") span(data-i18n="classes.diplomat_title")
@ -148,7 +131,7 @@ block content
| (Translator) | (Translator)
input#email_diplomatNews(name="email_diplomatNews", type="checkbox", checked=subs.diplomatNews) input#email_diplomatNews(name="email_diplomatNews", type="checkbox", checked=subs.diplomatNews)
span(data-i18n="contribute.diplomat_subscribe_desc").help-block Get emails about i18n developments and, eventually, levels to translate. span(data-i18n="contribute.diplomat_subscribe_desc").help-block Get emails about i18n developments and, eventually, levels to translate.
.form-group.checkbox .form-group.checkbox
label.control-label(for="email_ambassadorNews") label.control-label(for="email_ambassadorNews")
span(data-i18n="classes.ambassador_title") span(data-i18n="classes.ambassador_title")
@ -159,7 +142,6 @@ block content
input#email_ambassadorNews(name="email_ambassadorNews", type="checkbox", checked=subs.ambassadorNews) input#email_ambassadorNews(name="email_ambassadorNews", type="checkbox", checked=subs.ambassadorNews)
span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments. span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments.
button.btn#toggle-all-button(data-i18n="account_settings.email_toggle") Toggle All button#toggle-all-button.btn.btn-primary.form-control(data-i18n="account_settings.email_toggle") Toggle All
#job-profile-pane.tab-pane .clearfix
#job-profile-view

View file

@ -1,141 +0,0 @@
extends /templates/base
block content
if !me.isAnonymous()
.clearfix
.col-sm-6.clearfix
h2
span(data-i18n="account_settings.title") Account Settings
a.spl(href="/account/settings")
i.glyphicon.glyphicon-cog
hr
.row
.col-xs-6
.panel.panel-default
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-picture
a(href="account/settings#picture" data-i18n="account_settings.picture_tab") Picture
.panel-body.text-center
img#picture(src="#{me.getPhotoURL(150)}" alt="Picture")
.col-xs-6
.panel.panel-default
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-user
a(href="account/settings#wizard" data-i18n="account_settings.wizard_tab") Wizard
if (wizardSource)
.panel-body.text-center
img(src="#{wizardSource}")
.panel.panel-default.panel-me
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-user
a(href="account/settings#me" data-i18n="account_settings.me_tab") Me
.panel-body
table
tr
th(data-i18n="general.name") Name
td=me.displayName()
tr
th(data-i18n="general.email") Email
td=me.get('email')
.panel.panel-default.panel-emails
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-envelope
a(href="account/settings#emails" data-i18n="account_settings.emails_tab") Emails
.panel-body
if !hasEmailNotes && !hasEmailNews && !hasGeneralNews
p(data-i18n="account_settings.email_subscriptions_none") No email subscriptions.
if hasGeneralNews
h4(data-i18n="account_settings.email_news") News
ul
li(data-i18n="account_settings.email_announcements") Announcements
if hasEmailNotes
h4(data-i18n="account_settings.email_notifications") Notifications
ul
if subs.anyNotes
li(data-i18n="account_settings.email_any_notes") Any Notifications
if subs.recruitNotes
li(data-i18n="account_settings.email_recruit_notes") Job Opportunities
if hasEmailNews
h4(data-i18n="account_settings.contributor_emails") Contributor Emails
ul
if (subs.archmageNews)
li
span(data-i18n="classes.archmage_title")
| Archmage
span(data-i18n="classes.archmage_title_description")
| (Coder)
if (subs.artisanNews)
li
span.spr(data-i18n="classes.artisan_title")
| Artisan
span(data-i18n="classes.artisan_title_description")
| (Level Builder)
if (subs.adventurerNews)
li
span.spr(data-i18n="classes.adventurer_title")
| Adventurer
span(data-i18n="classes.adventurer_title_description")
| (Level Playtester)
if (subs.scribeNews)
li
span.spr(data-i18n="classes.scribe_title")
| Scribe
span(data-i18n="classes.scribe_title_description")
| (Article Editor)
if (subs.diplomatNews)
li
span.spr(data-i18n="classes.diplomat_title")
| Diplomat
span(data-i18n="classes.diplomat_title_description")
| (Translator)
if (subs.ambassadorNews)
li
span.spr(data-i18n="classes.ambassador_title")
| Ambassador
span(data-i18n="classes.ambassador_title_description")
| (Support)
.panel.panel-default
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-wrench
a(href="account/settings#password" data-i18n="general.password") Password
//.panel.panel-default
// .panel-heading
// h3.panel-title
// i.glyphicon.glyphicon-briefcase
// a(href="account/settings#job-profile" data-i18n="account_settings.job_profile") Job Profile
.col-sm-6
h2(data-i18n="user.recently_played") Recently Played
hr
if !recentlyPlayed
div(data-i18n="common.loading") Loading...
else if recentlyPlayed.length
table.table
tr
th(data-i18n="resources.level") Level
th(data-i18n="user.last_played") Last Played
th(data-i18n="user.status") Status
each session in recentlyPlayed
if session.get('levelName')
tr
td
- var posturl = ''
- if (session.get('team')) posturl = '?team=' + session.get('team')
a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '')
td= moment(session.get('changed')).fromNow()
if session.get('state').complete === true
td(data-i18n="user.status_completed") Completed
else if ! session.isMultiplayer()
td(data-i18n="user.status_unfinished") Unfinished
else
td
else
.panel.panel-default
.panel-body
div(data-i18n="account.no_recent_games") No games played during the past two weeks.

View file

@ -0,0 +1,21 @@
extends /templates/base
block content
if me.get('anonymous')
p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings.
else
ol.breadcrumb
li
a(href="/")
span.glyphicon.glyphicon-home
li.active(data-i18n="nav.account")
#account-links.panel.panel-default
.panel-heading(data-i18n="nav.account")
ul.list-group
li.list-group-item
a.btn.btn-lg.btn-primary(href="/account/settings", data-i18n="play.settings")
li.list-group-item
a.btn.btn-lg.btn-primary(href="/account/payments", data-i18n="account.payments")

View file

@ -0,0 +1,30 @@
extends /templates/base
block content
ol.breadcrumb
li
a(href="/")
span.glyphicon.glyphicon-home
li
a(href="/account", data-i18n="nav.account")
li.active(data-i18n="account.payments")
if payments.models.length
table.table.table-striped
tr
th(data-i18n="account.paid_on")
th(data-i18n="account.service")
th(data-i18n="account.price")
th(data-i18n="account.gems")
for payment in payments.models
- var service = payment.get('service')
tr
td= moment(payment.getCreationDate()).format('lll')
if service === 'ios'
td(data-i18n="account.service_apple")
td= payment.get('ios').localPrice
else
td(data-i18n="account.service_web")
td $#{(payment.get('amount')/100).toFixed(2)}
td= payment.get('gems')

View file

@ -1,7 +1,10 @@
block header block header
#site-nav #site-nav
img#nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat") a(href="/")
img#nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
div#site-nav-links div#site-nav-links
a(href="/")
img#small-nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
a(href="/") a(href="/")
span.glyphicon.glyphicon-home span.glyphicon.glyphicon-home
a(href="/about", data-i18n="nav.about") a(href="/about", data-i18n="nav.about")
@ -17,7 +20,7 @@ block header
img.account-settings-image(src=me.getPhotoURL(18), alt="") img.account-settings-image(src=me.getPhotoURL(18), alt="")
else else
i.glyphicon.glyphicon-user i.glyphicon.glyphicon-user
span.spl.spr(data-i18n="nav.account" href="/account") Account span.spl.spr(data-i18n="nav.account" href="/account")
span.caret span.caret
ul.dropdown-menu(role="menu") ul.dropdown-menu(role="menu")
li.user-dropdown-header li.user-dropdown-header
@ -25,18 +28,14 @@ block header
a(href="/user/#{me.getSlugOrID()}") a(href="/user/#{me.getSlugOrID()}")
img.img-circle(src="#{me.getPhotoURL()}" alt="") img.img-circle(src="#{me.getPhotoURL()}" alt="")
h3=me.displayName() h3=me.displayName()
li.user-dropdown-body li
.col-xs-4.text-center a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile")
a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile") Profile li
.col-xs-4.text-center a(href="/account/settings", data-i18n="play.settings")
a(href="/user/#{me.getSlugOrID()}/stats" data-i18n="nav.stats") Stats li
.col-xs-4.text-center a(href="/account/payments", data-i18n="account.payments")
a.disabled(data-i18n="nav.code") Code li
li.user-dropdown-footer a#logout-button(data-i18n="login.log_out")
.pull-left
a.btn.btn-default.btn-flat(href="/account" data-i18n="nav.account") Account
.pull-right
button#logout-button.btn.btn-default.btn-flat(data-i18n="login.log_out") Log Out
else else
button.btn.btn-sm.btn-primary.header-font.signup-button(data-i18n="login.sign_up") button.btn.btn-sm.btn-primary.header-font.signup-button(data-i18n="login.sign_up")

View file

@ -1,23 +1,26 @@
extends /templates/base extends /templates/base
block content block content
.progress if selectedLanguage
.progress-bar.progress-bar-info(role="progressbar" aria-valuenow=progress aria-valuemin="0" aria-valuemax="100" style="width: "+progress+"%")= progress+"%" .progress
.progress-bar.progress-bar-info(role="progressbar" aria-valuenow=progress aria-valuemin="0" aria-valuemax="100" style="width: "+progress+"%")= progress+"%"
table.table.table-condensed table.table.table-condensed
tr tr
th th
select#language-select.form-control.input-sm select#language-select.form-control.input-sm
option(value='') Select one...
th Type th Type
th Specifically Covered th Specifically Covered
th Generally Covered th Generally Covered
for model in collection.models if selectedLanguage
tr for model in collection.models
td tr
a(href=model.i18nURLBase+model.get('slug'))= model.get('name') td
td= model.constructor.className a(href=model.i18nURLBase+model.get('slug'))= model.get('name')
td(class=model.specificallyCovered ? 'success' : 'danger')= model.specificallyCovered ? 'Yes' : 'No' td= model.constructor.className
td(class=model.generallyCovered ? 'success' : 'danger')= model.generallyCovered ? 'Yes' : 'No' td(class=model.specificallyCovered ? 'success' : 'danger')= model.specificallyCovered ? 'Yes' : 'No'
td(class=model.generallyCovered ? 'success' : 'danger')= model.generallyCovered ? 'Yes' : 'No'

View file

@ -4,4 +4,7 @@ block modal-header-content
h3(data-i18n="play.account") Account h3(data-i18n="play.account") Account
block modal-body-content block modal-body-content
p TODO: show all dem account #account-settings-view
block modal-footer-content
#save-button.btn-lg.btn.disabled(data-i18n="general.save" disabled="true") No Changes

View file

@ -1,54 +0,0 @@
extends /templates/kinds/user
block append content
.btn-group.pull-right
button#grid-layout-button.btn.btn-default(data-layout='grid', class=activeLayout==='grid' ? 'active' : '')
i.glyphicon.glyphicon-th
button#table-layout-button.btn.btn-default(data-layout='table', class=activeLayout==='table' ? 'active' : '')
i.glyphicon.glyphicon-th-list
if achievementsByCategory
if activeLayout === 'grid'
.grid-layout
each achievements, category in achievementsByCategory
.row
h2.achievement-category-title(data-i18n="achievements.category_#{category}")=category
each achievement, index in achievements
- var title = achievement.i18nName();
- var description = achievement.i18nDescription();
- var locked = ! achievement.get('unlocked');
- var style = achievement.getStyle()
- var imgURL = achievement.getImageURL();
if locked
- var imgURL = achievement.getLockedImageURL();
else
- var imgURL = achievement.getImageURL();
.col-lg-4.col-xs-12
include ../achievements/achievement-popup
else if activeLayout === 'table'
.table-layout
if earnedAchievements.length
table.table
tr
th(data-i18n="general.name") Name
th(data-i18n="general.description") Description
th(data-i18n="general.date") Date
th(data-i18n="achievements.amount_achieved") Amount
th XP
each earnedAchievement in earnedAchievements.models
- var achievement = earnedAchievement.get('achievement');
if achievement.get('category')
// No level-specific achievements in here.
tr
td= achievement.i18nName()
td= achievement.i18nDescription()
td= moment().format("MMMM Do YYYY", earnedAchievement.get('changed'))
if achievement.isRepeatable()
td= earnedAchievement.get('achievedAmount')
else
td
td= earnedAchievement.get('earnedPoints')
else
.panel#no-achievements
.panel-body(data-i18n="user.no_achievements") No achievements earned yet.
else
div How did you even do that?

View file

@ -14,17 +14,8 @@ block append content
span(data-i18n="user.favorite_prefix") Favorite language is span(data-i18n="user.favorite_prefix") Favorite language is
strong.favorite-language= favoriteLanguage strong.favorite-language= favoriteLanguage
span(data-i18n="user.favorite_postfix") . span(data-i18n="user.favorite_postfix") .
.btn-group-vertical.profile-menu - var emails = user.getEnabledEmails()
a.btn.btn-default(href="/user/#{user.getSlugOrID()}/profile") // TODO: fix this, use some other method for finding contributor classes other than email settings, since they're private... Maybe achievements?
i.glyphicon.glyphicon-briefcase
span(data-i18n="account_settings.job_profile") Job Profile
a.btn.btn-default(href="/user/#{user.getSlugOrID()}/stats")
i.glyphicon.glyphicon-certificate
span(data-i18n="user.stats") Stats
a.btn.btn-default.disabled(href="#")
i.glyphicon.glyphicon-pencil
span(data-i18n="general.code") Code
- var emails = user.get('emails')
if emails if emails
ul.contributor-categories ul.contributor-categories
//li.contributor-category //li.contributor-category
@ -69,9 +60,11 @@ block append content
th.col-xs-4(data-i18n="resources.level") Level th.col-xs-4(data-i18n="resources.level") Level
th.col-xs-4(data-i18n="user.last_played") Last Played th.col-xs-4(data-i18n="user.last_played") Last Played
th.col-xs-4(data-i18n="user.status") Status th.col-xs-4(data-i18n="user.status") Status
each session in singlePlayerSessions - var count = 0
each session, index in singlePlayerSessions
if session.get('levelName') if session.get('levelName')
tr tr(class=count > 4 ? 'hide' : '')
- count++;
td td
a(href="/play/level/#{session.get('levelID')}")= session.get('levelName') a(href="/play/level/#{session.get('levelID')}")= session.get('levelName')
td= moment(session.get('changed')).fromNow() td= moment(session.get('changed')).fromNow()
@ -79,6 +72,9 @@ block append content
td(data-i18n="user.status_completed") Completed td(data-i18n="user.status_completed") Completed
else else
td(data-i18n="user.status_unfinished") Unfinished td(data-i18n="user.status_unfinished") Unfinished
if count > 4
.panel-footer
button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more")
else else
.panel-body .panel-body
p(data-i18n="user.no_singleplayer") No Singleplayer games played yet. p(data-i18n="user.no_singleplayer") No Singleplayer games played yet.
@ -94,17 +90,20 @@ block append content
th.col-xs-4(data-i18n="resources.level") Level th.col-xs-4(data-i18n="resources.level") Level
th.col-xs-4(data-i18n="user.last_played") Last Played th.col-xs-4(data-i18n="user.last_played") Last Played
th.col-xs-4(data-i18n="general.score") Score th.col-xs-4(data-i18n="general.score") Score
each session in multiPlayerSessions each session, index in multiPlayerSessions
tr tr(class=index > 4 ? 'hide' : '')
td td
- var posturl = '' - var posturl = ''
- if (session.get('team')) posturl = '?team=' + session.get('team') - if (session.get('team')) posturl = '?team=' + session.get('team')
a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '') a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '')
td= moment(session.get('changed')).fromNow() td= moment(session.get('changed')).fromNow()
if session.get('totalScore') if session.get('totalScore')
td= session.get('totalScore') * 100 td= parseInt(session.get('totalScore') * 100)
else else
td(data-i18n="user.status_unfinished") Unfinished td(data-i18n="user.status_unfinished") Unfinished
if multiPlayerSessions.length > 4
.panel-footer
button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more")
else else
.panel-body .panel-body
p(data-i18n="user.no_multiplayer") No Multiplayer games played yet. p(data-i18n="user.no_multiplayer") No Multiplayer games played yet.
@ -123,11 +122,15 @@ block append content
th.col-xs-4(data-i18n="achievements.achievement") Achievement th.col-xs-4(data-i18n="achievements.achievement") Achievement
th.col-xs-4(data-i18n="achievements.last_earned") Last Earned th.col-xs-4(data-i18n="achievements.last_earned") Last Earned
th.col-xs-4(data-i18n="achievements.amount_achieved") Amount th.col-xs-4(data-i18n="achievements.amount_achieved") Amount
each achievement in earnedAchievements.models each achievement, index in earnedAchievements.models
tr tr(class=index > 4 ? 'hide' : '')
td= achievement.get('achievementName') td= achievement.get('achievementName')
td= moment().format("MMMM Do YYYY", achievement.get('changed')) td= moment().format("MMMM Do YYYY", achievement.get('changed'))
if achievement.get('achievedAmount') if achievement.get('achievedAmount')
td= achievement.get('achievedAmount') td= achievement.get('achievedAmount')
else else
td td
if earnedAchievements.length > 4
.panel-footer
button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more")

View file

@ -0,0 +1,48 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/account/account-settings-root-view'
AccountSettingsView = require './AccountSettingsView'
module.exports = class AccountSettingsRootView extends RootView
id: "account-settings-root-view"
template: template
events:
'click #save-button': -> @accountSettingsView.save()
shortcuts:
'enter': -> @
afterRender: ->
super()
@accountSettingsView = new AccountSettingsView()
@insertSubView(@accountSettingsView)
@listenTo @accountSettingsView, 'input-changed', @onInputChanged
@listenTo @accountSettingsView, 'save-user-began', @onUserSaveBegan
@listenTo @accountSettingsView, 'save-user-success', @onUserSaveSuccess
@listenTo @accountSettingsView, 'save-user-error', @onUserSaveError
onInputChanged: ->
@$el.find('#save-button')
.text($.i18n.t('common.save', defaultValue: 'Save'))
.addClass 'btn-info'
.removeClass 'disabled btn-danger'
.removeAttr 'disabled'
onUserSaveBegan: ->
@$el.find('#save-button')
.text($.i18n.t('common.saving', defaultValue: 'Saving...'))
.removeClass('btn-danger')
.addClass('btn-success').show()
onUserSaveSuccess: ->
@$el.find('#save-button')
.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved'))
.removeClass('btn-success btn-info', 1000)
.attr('disabled', 'true')
onUserSaveError: ->
@$el.find('#save-button')
.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving'))
.removeClass('btn-success')
.addClass('btn-danger', 500)

View file

@ -1,72 +1,48 @@
RootView = require 'views/kinds/RootView' CocoView = require 'views/kinds/CocoView'
template = require 'templates/account/settings' template = require 'templates/account/account-settings-view'
{me} = require 'lib/auth' {me} = require 'lib/auth'
forms = require 'lib/forms' forms = require 'lib/forms'
User = require 'models/User' User = require 'models/User'
AuthModal = require 'views/modal/AuthModal' AuthModal = require 'views/modal/AuthModal'
WizardSettingsView = require './WizardSettingsView' module.exports = class AccountSettingsView extends CocoView
JobProfileTreemaView = require './JobProfileTreemaView'
module.exports = class AccountSettingsView extends RootView
id: 'account-settings-view' id: 'account-settings-view'
template: template template: template
changedFields: [] # DOM input fields className: 'countainer-fluid'
events: events:
'click #save-button': 'save' 'change .panel input': 'onInputChanged'
'change #settings-panes input:checkbox': (e) -> @trigger 'checkboxToggled', e 'change #name': 'checkNameExists'
'keyup #settings-panes input:text, #settings-panes input:password': (e) -> @trigger 'inputChanged', e
'keyup #name': 'onNameChange'
'click #toggle-all-button': 'toggleEmailSubscriptions' 'click #toggle-all-button': 'toggleEmailSubscriptions'
'keypress #settings-panes': 'onKeyPress' 'click .profile-photo': 'onEditProfilePhoto'
'click #upload-photo-button': 'onEditProfilePhoto'
constructor: (options) -> constructor: (options) ->
@save = _.debounce(@save, 200)
@onNameChange = _.debounce @checkNameExists, 500
super options super options
return unless me require('lib/services/filepicker')() unless window.application.isIPadApp # Initialize if needed
@uploadFilePath = "db/user/#{me.id}"
@listenTo(me, 'invalid', (errors) -> forms.applyErrorsToForm(@$el, me.validationError)) afterInsert: ->
@on 'checkboxToggled', @onToggle super()
@on 'checkboxToggled', @onInputChanged @openModalView new AuthModal() if me.get('anonymous')
@on 'inputChanged', @onInputChanged
@on 'enterPressed', @onEnter
getRenderData: ->
c = super()
return c unless me
c.subs = {}
c.subs[sub] = 1 for sub in me.getEnabledEmails()
c
#- Form input callbacks
onInputChanged: (e) -> onInputChanged: (e) ->
return @enableSaveButton() unless e?.currentTarget $(e.target).addClass 'changed'
that = e.currentTarget @trigger 'input-changed'
$that = $(that)
savedValue = $that.data 'saved-value'
currentValue = $that.val()
if savedValue isnt currentValue
@changedFields.push that unless that in @changedFields
@enableSaveButton()
else
_.pull @changedFields, that
@disableSaveButton() if _.isEmpty @changedFields
onToggle: (e) -> toggleEmailSubscriptions: =>
$that = $(e.currentTarget) subs = @getSubscriptions()
$that.val $that[0].checked $('#email-panel input[type="checkbox"]', @$el).prop('checked', not _.any(_.values(subs))).addClass('changed')
onEnter: ->
@save()
onKeyPress: (e) ->
@trigger 'enterPressed', e if e.which is 13
enableSaveButton: ->
$('#save-button', @$el).removeClass 'disabled'
$('#save-button', @$el).removeClass 'btn-danger'
$('#save-button', @$el).removeAttr 'disabled'
$('#save-button', @$el).text 'Save'
disableSaveButton: ->
$('#save-button', @$el).addClass 'disabled'
$('#save-button', @$el).removeClass 'btn-danger'
$('#save-button', @$el).attr 'disabled', "true"
$('#save-button', @$el).text 'No Changes'
checkNameExists: => checkNameExists: =>
name = $('#name', @$el).val() name = $('#name', @$el).val()
@ -79,88 +55,53 @@ module.exports = class AccountSettingsView extends RootView
@suggestedName = newName @suggestedName = newName
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?", true forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?", true
afterRender: ->
super()
$('#settings-tabs a', @$el).click((e) =>
e.preventDefault()
$(e.target).tab('show')
# make sure errors show up in the general pane, but keep the password pane clean
$('#password-pane input').val('')
#@save() unless $(e.target).attr('href') is '#password-pane'
forms.clearFormAlerts($('#password-pane', @$el))
)
@chooseTab(location.hash.replace('#', ''))
wizardSettingsView = new WizardSettingsView()
@listenTo wizardSettingsView, 'change', @enableSaveButton
@insertSubView wizardSettingsView
@jobProfileTreemaView = new JobProfileTreemaView()
@listenTo @jobProfileTreemaView, 'change', @enableSaveButton
@insertSubView @jobProfileTreemaView
_.defer => @buildPictureTreema() # Not sure why, but the Treemas don't fully build without this if you reload the page.
afterInsert: ->
super()
$('#email-pane input[type="checkbox"]').on 'change', ->
$(@).addClass 'changed'
if me.get('anonymous')
@openModalView new AuthModal()
@updateSavedValues()
chooseTab: (category) ->
id = "##{category}-pane"
pane = $(id, @$el)
return @chooseTab('general') unless pane.length or category is 'general'
loc = "a[href=#{id}]"
$(loc, @$el).tab('show')
$('.tab-pane').removeClass('active')
pane.addClass('active')
@currentTab = category
getRenderData: ->
c = super()
return c unless me
c.subs = {}
c.subs[sub] = 1 for sub in c.me.getEnabledEmails()
c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1
c
getSubscriptions: ->
inputs = ($(i) for i in $('#email-pane input[type="checkbox"].changed', @$el))
emailNames = (i.attr('name').replace('email_', '') for i in inputs)
enableds = (i.prop('checked') for i in inputs)
_.zipObject emailNames, enableds
toggleEmailSubscriptions: =>
subs = @getSubscriptions()
$('#email-pane input[type="checkbox"]', @$el).prop('checked', not _.any(_.values(subs))).addClass('changed')
@save()
buildPictureTreema: ->
data = photoURL: me.get('photoURL')
data.photoURL = null if data.photoURL?.search('gravatar') isnt -1 # Old style
schema = $.extend true, {}, me.schema()
schema.properties = _.pick me.schema().properties, 'photoURL'
schema.required = ['photoURL']
treemaOptions =
filePath: "db/user/#{me.id}"
schema: schema
data: data
callbacks: {change: @onPictureChanged}
@pictureTreema = @$el.find('#picture-treema').treema treemaOptions
@pictureTreema?.build()
@pictureTreema?.open()
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
onPictureChanged: (e) => onPictureChanged: (e) =>
@trigger 'inputChanged', e @trigger 'inputChanged', e
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL' @$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
save: (e) ->
#- Just copied from OptionsView, TODO refactor
onEditProfilePhoto: (e) ->
return if window.application.isIPadApp # TODO: have an iPad-native way of uploading a photo, since we don't want to load FilePicker on iPad (memory)
photoContainer = @$el.find('.profile-photo')
onSaving = =>
photoContainer.addClass('saving')
onSaved = (uploadingPath) =>
@$el.find('#photoURL').val(uploadingPath)
@onInputChanged() # cause for some reason editing the value doesn't trigger the jquery event
me.set('photoURL', uploadingPath)
photoContainer.removeClass('saving').attr('src', me.getPhotoURL(photoContainer.width()))
filepicker.pick {mimetypes: 'image/*'}, @onImageChosen(onSaving, onSaved)
formatImagePostData: (inkBlob) ->
url: inkBlob.url, filename: inkBlob.filename, mimetype: inkBlob.mimetype, path: @uploadFilePath, force: true
onImageChosen: (onSaving, onSaved) ->
(inkBlob) =>
onSaving()
uploadingPath = [@uploadFilePath, inkBlob.filename].join('/')
data = @formatImagePostData(inkBlob)
success = @onImageUploaded(onSaved, uploadingPath)
$.ajax '/file', type: 'POST', data: data, success: success
onImageUploaded: (onSaved, uploadingPath) ->
(e) =>
onSaved uploadingPath
#- Misc
getSubscriptions: ->
inputs = ($(i) for i in $('#email-panel input[type="checkbox"].changed', @$el))
emailNames = (i.attr('name').replace('email_', '') for i in inputs)
enableds = (i.prop('checked') for i in inputs)
_.zipObject emailNames, enableds
#- Saving changes
save: ->
$('#settings-tabs input').removeClass 'changed' $('#settings-tabs input').removeClass 'changed'
forms.clearFormAlerts(@$el) forms.clearFormAlerts(@$el)
@grabData() @grabData()
@ -168,23 +109,23 @@ module.exports = class AccountSettingsView extends RootView
if res? if res?
console.error 'Couldn\'t save because of validation errors:', res console.error 'Couldn\'t save because of validation errors:', res
forms.applyErrorsToForm(@$el, res) forms.applyErrorsToForm(@$el, res)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
return return
return unless me.hasLocalChanges() return unless me.hasLocalChanges()
res = me.patch() res = me.patch()
return unless res return unless res
save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...'))
.removeClass('btn-danger').addClass('btn-success').show()
res.error -> res.error =>
errors = JSON.parse(res.responseText) errors = JSON.parse(res.responseText)
forms.applyErrorsToForm(@$el, errors) forms.applyErrorsToForm(@$el, errors)
save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-success').addClass('btn-danger', 500) $('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
@trigger 'save-user-error'
res.success (model, response, options) => res.success (model, response, options) =>
@changedFields = [] @trigger 'save-user-success'
@updateSavedValues()
save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-success', 500).attr('disabled', 'true') @trigger 'save-user-began'
grabData: -> grabData: ->
@grabPasswordData() @grabPasswordData()
@ -198,6 +139,7 @@ module.exports = class AccountSettingsView extends RootView
message = $.i18n.t('account_settings.password_mismatch', defaultValue: 'Password does not match.') message = $.i18n.t('account_settings.password_mismatch', defaultValue: 'Password does not match.')
err = [message: message, property: 'password2', formatted: true] err = [message: message, property: 'password2', formatted: true]
forms.applyErrorsToForm(@$el, err) forms.applyErrorsToForm(@$el, err)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
return return
if bothThere if bothThere
me.set('password', password1) me.set('password', password1)
@ -205,36 +147,19 @@ module.exports = class AccountSettingsView extends RootView
message = $.i18n.t('account_settings.password_repeat', defaultValue: 'Please repeat your password.') message = $.i18n.t('account_settings.password_repeat', defaultValue: 'Please repeat your password.')
err = [message: message, property: 'password2', formatted: true] err = [message: message, property: 'password2', formatted: true]
forms.applyErrorsToForm(@$el, err) forms.applyErrorsToForm(@$el, err)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
grabOtherData: -> grabOtherData: ->
$('#name', @$el).val @suggestedName if @suggestedName @$el.find('#name').val @suggestedName if @suggestedName
me.set 'name', $('#name', @$el).val() me.set 'name', @$el.find('#name').val()
me.set 'email', $('#email', @$el).val() me.set 'email', @$el.find('#email').val()
for emailName, enabled of @getSubscriptions() for emailName, enabled of @getSubscriptions()
me.setEmailSubscription emailName, enabled me.setEmailSubscription emailName, enabled
me.set 'photoURL', @pictureTreema.get('/photoURL')
me.set('photoURL', @$el.find('#photoURL').val())
adminCheckbox = @$el.find('#admin') adminCheckbox = @$el.find('#admin')
if adminCheckbox.length if adminCheckbox.length
permissions = [] permissions = []
permissions.push 'admin' if adminCheckbox.prop('checked') permissions.push 'admin' if adminCheckbox.prop('checked')
me.set('permissions', permissions) me.set('permissions', permissions)
jobProfile = me.get('jobProfile') ? {}
updated = false
for key, val of @jobProfileTreemaView.getData()
updated = updated or not _.isEqual jobProfile[key], val
jobProfile[key] = val
if updated
jobProfile.updated = (new Date()).toISOString()
me.set 'jobProfile', jobProfile
updateSavedValues: ->
$('#settings-panes input:text').each ->
$(@).data 'saved-value', $(@).val()
$('#settings-panes input:checkbox').each ->
$(@).data 'saved-value', JSON.stringify $(@)[0].checked
destroy: ->
@pictureTreema?.destroy()
super()

View file

@ -1,38 +1,6 @@
View = require 'views/kinds/RootView' RootView = require 'views/kinds/RootView'
template = require 'templates/account/account_home' template = require 'templates/account/main-account-view'
{me} = require 'lib/auth'
User = require 'models/User'
AuthModalView = require 'views/modal/AuthModal'
RecentlyPlayedCollection = require 'collections/RecentlyPlayedCollection'
ThangType = require 'models/ThangType'
module.exports = class MainAccountView extends View module.exports = class MainAccountView extends RootView
id: 'account-home' id: 'main-account-view'
template: template template: template
constructor: (options) ->
super options
return unless me
@wizardType = ThangType.loadUniversalWizard()
@recentlyPlayed = new RecentlyPlayedCollection me.get('_id')
@supermodel.loadModel @wizardType, 'thang'
@supermodel.loadCollection @recentlyPlayed, 'recentlyPlayed'
onLoaded: ->
super()
getRenderData: ->
c = super()
c.subs = {}
enabledEmails = c.me.getEnabledEmails()
c.subs[sub] = 1 for sub in enabledEmails
c.hasEmailNotes = _.any enabledEmails, (sub) -> sub.contains 'Notes'
c.hasEmailNews = _.any enabledEmails, (sub) -> sub.contains('News') and sub isnt 'generalNews'
c.hasGeneralNews = 'generalNews' in enabledEmails
c.wizardSource = @wizardType.getPortraitSource colorConfig: me.get('wizard')?.colorConfig if @wizardType.loaded
c.recentlyPlayed = @recentlyPlayed.models
c
afterRender: ->
super()
@openModalView new AuthModalView if me.isAnonymous()

View file

@ -0,0 +1,18 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/account/payments-view'
CocoCollection = require 'collections/CocoCollection'
Payment = require 'models/Payment'
module.exports = class PaymentsView extends RootView
id: "payments-view"
template: template
constructor: (options) ->
super(options)
@payments = new CocoCollection([], { url: '/db/payment', model: Payment })
@supermodel.loadCollection(@payments, 'payments')
getRenderData: ->
c = super()
c.payments = @payments
c

View file

@ -47,6 +47,7 @@ module.exports = class I18NEditModelView extends RootView
@hush = true @hush = true
$select = @$el.find('#language-select').empty() $select = @$el.find('#language-select').empty()
@addLanguagesToSelect($select, @selectedLanguage) @addLanguagesToSelect($select, @selectedLanguage)
@$el.find('option[value="en-US"]').remove()
@hush = false @hush = false
editors = [] editors = []
@ -123,6 +124,9 @@ module.exports = class I18NEditModelView extends RootView
onLanguageSelectChanged: (e) -> onLanguageSelectChanged: (e) ->
return if @hush return if @hush
@selectedLanguage = $(e.target).val() @selectedLanguage = $(e.target).val()
if @selectedLanguage
me.set('preferredLanguage', @selectedLanguage)
me.patch()
@render() @render()
onSubmitPatch: (e) -> onSubmitPatch: (e) ->

View file

@ -19,7 +19,7 @@ module.exports = class I18NHomeView extends RootView
constructor: (options) -> constructor: (options) ->
super(options) super(options)
@selectedLanguage = me.get('preferredLanguage', true) @selectedLanguage = me.get('preferredLanguage') or ''
#- #-
@aggregateModels = new Backbone.Collection() @aggregateModels = new Backbone.Collection()
@ -92,7 +92,12 @@ module.exports = class I18NHomeView extends RootView
afterRender: -> afterRender: ->
super() super()
@addLanguagesToSelect(@$el.find('#language-select'), @selectedLanguage) @addLanguagesToSelect(@$el.find('#language-select'), @selectedLanguage)
@$el.find('option[value="en-US"]').remove()
onLanguageSelectChanged: (e) -> onLanguageSelectChanged: (e) ->
@selectedLanguage = $(e.target).val() @selectedLanguage = $(e.target).val()
if @selectedLanguage
# simplest solution, see if this actually ends up being not what people want
me.set('preferredLanguage', @selectedLanguage)
me.patch()
@render() @render()

View file

@ -76,7 +76,7 @@ module.exports = class RootView extends CocoView
@openModalView new ModalClass {} @openModalView new ModalClass {}
showLoading: ($el) -> showLoading: ($el) ->
$el ?= @$el.find('.main-content-area') $el ?= @$el.find('#site-content-area')
super($el) super($el)
afterInsert: -> afterInsert: ->

View file

@ -25,6 +25,7 @@ module.exports = class AuthModal extends ModalView
'auth:logging-in-with-facebook': 'onLoggingInWithFacebook' 'auth:logging-in-with-facebook': 'onLoggingInWithFacebook'
constructor: (options) -> constructor: (options) ->
options ?= {}
@onNameChange = _.debounce @checkNameExists, 500 @onNameChange = _.debounce @checkNameExists, 500
super options super options
@mode = options.mode if options.mode @mode = options.mode if options.mode

View file

@ -1,15 +1,15 @@
ModalView = require 'views/kinds/ModalView' ModalView = require 'views/kinds/ModalView'
template = require 'templates/play/modal/play-account-modal' template = require 'templates/play/modal/play-account-modal'
AccountSettingsView = require 'views/account/AccountSettingsView'
module.exports = class PlayAccountModal extends ModalView module.exports = class PlayAccountModal extends ModalView
className: 'modal fade play-modal' className: 'modal fade play-modal'
template: template template: template
modalWidthPercent: 90 plain: true
id: 'play-account-modal' id: 'play-account-modal'
#instant: true
#events: events:
# 'change input.select': 'onSelectionChanged' 'click #save-button': -> @accountSettingsView.save()
constructor: (options) -> constructor: (options) ->
super options super options
@ -22,7 +22,32 @@ module.exports = class PlayAccountModal extends ModalView
super() super()
return unless @supermodel.finished() return unless @supermodel.finished()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-open', volume: 1 Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-open', volume: 1
@accountSettingsView = new AccountSettingsView()
@insertSubView(@accountSettingsView)
@listenTo @accountSettingsView, 'input-changed', @onInputChanged
@listenTo @accountSettingsView, 'save-user-began', @onUserSaveBegan
@listenTo @accountSettingsView, 'save-user-success', @hide
@listenTo @accountSettingsView, 'save-user-error', @onUserSaveError
onHidden: -> onHidden: ->
super() super()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1 Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1
onInputChanged: ->
@$el.find('#save-button')
.text($.i18n.t('common.save', defaultValue: 'Save'))
.addClass 'btn-info'
.removeClass 'disabled btn-danger'
.removeAttr 'disabled'
onUserSaveBegan: ->
@$el.find('#save-button')
.text($.i18n.t('common.saving', defaultValue: 'Saving...'))
.removeClass('btn-danger')
.addClass('btn-success').show()
onUserSaveError: ->
@$el.find('#save-button')
.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving'))
.removeClass('btn-success')
.addClass('btn-danger', 500)

View file

@ -54,7 +54,6 @@ module.exports = class PlayItemsModal extends ModalView
constructor: (options) -> constructor: (options) ->
super options super options
me.set('spent', 0)
@items = new Backbone.Collection() @items = new Backbone.Collection()
@itemCategoryCollections = {} @itemCategoryCollections = {}

View file

@ -1,55 +0,0 @@
UserView = require 'views/kinds/UserView'
template = require 'templates/user/achievements'
{me} = require 'lib/auth'
Achievement = require 'models/Achievement'
EarnedAchievement = require 'models/EarnedAchievement'
AchievementCollection = require 'collections/AchievementCollection'
EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
module.exports = class AchievementsView extends UserView
id: 'user-achievements-view'
template: template
viewName: 'Stats'
activeLayout: 'grid'
events:
'click #grid-layout-button': 'layoutChanged'
'click #table-layout-button': 'layoutChanged'
constructor: (userID, options) ->
super options, userID
onLoaded: ->
unless @achievements or @earnedAchievements
@supermodel.resetProgress()
@achievements = new AchievementCollection
@earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID()
@supermodel.loadCollection @achievements, 'achievements'
@supermodel.loadCollection @earnedAchievements, 'earnedAchievements'
else
for earned in @earnedAchievements.models
return unless relatedAchievement = _.find @achievements.models, (achievement) ->
achievement.get('_id') is earned.get 'achievement'
relatedAchievement.set 'unlocked', true
earned.set 'achievement', relatedAchievement
deferredImages = (achievement.cacheLockedImage() for achievement in @achievements.models when not achievement.get 'unlocked')
whenever = $.when deferredImages...
whenever.done => @render()
super()
layoutChanged: (e) ->
@activeLayout = $(e.currentTarget).data 'layout'
@render()
getRenderData: ->
context = super()
context.activeLayout = @activeLayout
# After user is loaded
if @user and not @user.isAnonymous()
context.earnedAchievements = @earnedAchievements
context.achievementsByCategory = {}
for achievement in @achievements.models when achievement.get('category')
context.achievementsByCategory[achievement.get('category')] ?= []
context.achievementsByCategory[achievement.get('category')].push achievement
context

View file

@ -1,7 +1,7 @@
UserView = require 'views/kinds/UserView' UserView = require 'views/kinds/UserView'
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
LevelSession = require 'models/LevelSession' LevelSession = require 'models/LevelSession'
template = require 'templates/user/user_home' template = require 'templates/user/main-user-view'
{me} = require 'lib/auth' {me} = require 'lib/auth'
EarnedAchievementCollection = require 'collections/EarnedAchievementCollection' EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
@ -15,6 +15,9 @@ class LevelSessionsCollection extends CocoCollection
module.exports = class MainUserView extends UserView module.exports = class MainUserView extends UserView
id: 'user-home' id: 'user-home'
template: template template: template
events:
'click .more-button': 'onClickMoreButton'
constructor: (userID, options) -> constructor: (userID, options) ->
super options super options
@ -54,3 +57,8 @@ module.exports = class MainUserView extends UserView
@supermodel.loadCollection @levelSessions, 'levelSessions' @supermodel.loadCollection @levelSessions, 'levelSessions'
@supermodel.loadCollection @earnedAchievements, 'earnedAchievements' @supermodel.loadCollection @earnedAchievements, 'earnedAchievements'
super() super()
onClickMoreButton: (e) ->
panel = $(e.target).closest('.panel')
panel.find('tr.hide').removeClass('hide')
panel.find('.panel-footer').remove()

View file

@ -48,7 +48,13 @@ module.exports = class Handler
flattened = deltasLib.flattenDelta(delta) flattened = deltasLib.flattenDelta(delta)
_.all flattened, (delta) -> _.all flattened, (delta) ->
# sometimes coverage gets moved around... allow other changes to happen to i18nCoverage # sometimes coverage gets moved around... allow other changes to happen to i18nCoverage
return _.isArray(delta.o) and (('i18n' in delta.dataPath and delta.o.length is 1) or 'i18nCoverage' in delta.dataPath) return false unless _.isArray(delta.o)
return true if 'i18nCoverage' in delta.dataPath
return false unless delta.o.length is 1
index = delta.deltaPath.indexOf('i18n')
return false if index is -1
return false if delta.deltaPath[index+1] is 'en-US'
return true
formatEntity: (req, document) -> document?.toObject() formatEntity: (req, document) -> document?.toObject()
getEditableProperties: (req, document) -> getEditableProperties: (req, document) ->

View file

@ -310,6 +310,7 @@ LevelHandler = class LevelHandler extends Handler
@sendSuccess res, data @sendSuccess res, data
hasAccessToDocument: (req, document, method=null) -> hasAccessToDocument: (req, document, method=null) ->
method ?= req.method
return true if method is null or method is 'get' return true if method is null or method is 'get'
super(req, document, method) super(req, document, method)

View file

@ -17,13 +17,13 @@ products = {
gems: 5000 gems: 5000
id: 'gems_5' id: 'gems_5'
} }
'gems_10': { 'gems_10': {
amount: 999 amount: 999
gems: 11000 gems: 11000
id: 'gems_10' id: 'gems_10'
} }
'gems_20': { 'gems_20': {
amount: 1999 amount: 1999
gems: 25000 gems: 25000
@ -43,7 +43,7 @@ PaymentHandler = class PaymentHandler extends Handler
payment.set 'recipient', req.user._id payment.set 'recipient', req.user._id
payment.set 'created', new Date().toISOString() payment.set 'created', new Date().toISOString()
payment payment
post: (req, res) -> post: (req, res) ->
appleReceipt = req.body.apple?.rawReceipt appleReceipt = req.body.apple?.rawReceipt
appleTransactionID = req.body.apple?.transactionID appleTransactionID = req.body.apple?.transactionID
@ -51,46 +51,46 @@ PaymentHandler = class PaymentHandler extends Handler
stripeToken = req.body.stripe?.token stripeToken = req.body.stripe?.token
stripeTimestamp = parseInt(req.body.stripe?.timestamp) stripeTimestamp = parseInt(req.body.stripe?.timestamp)
productID = req.body.productID productID = req.body.productID
if not (appleReceipt or (stripeTimestamp and productID)) if not (appleReceipt or (stripeTimestamp and productID))
return @sendBadInputError(res, 'Need either apple.rawReceipt or stripe.timestamp and productID') return @sendBadInputError(res, 'Need either apple.rawReceipt or stripe.timestamp and productID')
if stripeTimestamp and not productID if stripeTimestamp and not productID
return @sendBadInputError(res, 'Need productID if paying with Stripe.') return @sendBadInputError(res, 'Need productID if paying with Stripe.')
if stripeTimestamp and (not stripeToken) and (not user.get('stripeCustomerID')) if stripeTimestamp and (not stripeToken) and (not user.get('stripeCustomerID'))
return @sendBadInputError(res, 'Need stripe.token if new customer.') return @sendBadInputError(res, 'Need stripe.token if new customer.')
if appleReceipt if appleReceipt
if not appleTransactionID if not appleTransactionID
return @sendBadInputError(res, 'Apple purchase? Need to specify which transaction.') return @sendBadInputError(res, 'Apple purchase? Need to specify which transaction.')
@handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice) @handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice)
else else
@handleStripePaymentPost(req, res, stripeTimestamp, productID, stripeToken) @handleStripePaymentPost(req, res, stripeTimestamp, productID, stripeToken)
#- Apple payments #- Apple payments
handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) -> handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) ->
formFields = { 'receipt-data': receipt } formFields = { 'receipt-data': receipt }
#- verify receipt with Apple #- verify receipt with Apple
verifyReq = request.post({url: config.apple.verifyURL, json: formFields}, (err, verifyRes, body) => verifyReq = request.post({url: config.apple.verifyURL, json: formFields}, (err, verifyRes, body) =>
if err or not body?.receipt?.in_app or (not body?.bundle_id is 'com.codecombat.CodeCombat') if err or not body?.receipt?.in_app or (not body?.bundle_id is 'com.codecombat.CodeCombat')
console.warn 'apple receipt error?', err, body console.warn 'apple receipt error?', err, body
@sendBadInputError(res, 'Unable to verify Apple receipt.') @sendBadInputError(res, 'Unable to verify Apple receipt.')
return return
transaction = _.find body.receipt.in_app, { transaction_id: transactionID } transaction = _.find body.receipt.in_app, { transaction_id: transactionID }
return @sendBadInputError(res, 'Invalid transactionID.') unless transaction return @sendBadInputError(res, 'Invalid transactionID.') unless transaction
#- Check existence #- Check existence
transactionID = transaction.transaction_id transactionID = transaction.transaction_id
criteria = { 'ios.transactionID': transactionID } criteria = { 'ios.transactionID': transactionID }
Payment.findOne(criteria).exec((err, payment) => Payment.findOne(criteria).exec((err, payment) =>
if payment if payment
unless payment.get('recipient').equals(req.user._id) unless payment.get('recipient').equals(req.user._id)
return @sendForbiddenError(res) return @sendForbiddenError(res)
@ -119,17 +119,18 @@ PaymentHandler = class PaymentHandler extends Handler
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@incrementGemsFor(req.user, product.gems, (err) => @incrementGemsFor(req.user, product.gems, (err) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@sendPaymentHipChatMessage user: req.user, payment: payment
@sendCreated(res, @formatEntity(req, payment)) @sendCreated(res, @formatEntity(req, payment))
) )
) )
) )
) )
#- Stripe payments #- Stripe payments
handleStripePaymentPost: (req, res, timestamp, productID, token) -> handleStripePaymentPost: (req, res, timestamp, productID, token) ->
# First, make sure we save the payment info as a Customer object, if we haven't already. # First, make sure we save the payment info as a Customer object, if we haven't already.
if not req.user.get('stripeCustomerID') if not req.user.get('stripeCustomerID')
stripe.customers.create({ stripe.customers.create({
@ -145,10 +146,10 @@ PaymentHandler = class PaymentHandler extends Handler
(err) => (err) =>
return @sendDatabaseError(res, err) return @sendDatabaseError(res, err)
) )
else else
@beginStripePayment(req, res, timestamp, productID) @beginStripePayment(req, res, timestamp, productID)
beginStripePayment: (req, res, timestamp, productID) -> beginStripePayment: (req, res, timestamp, productID) ->
product = products[productID] product = products[productID]
@ -168,29 +169,30 @@ PaymentHandler = class PaymentHandler extends Handler
) )
) )
], ],
((err, results) => ((err, results) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
[payment, charge] = results [payment, charge] = results
if not (payment or charge) if not (payment or charge)
# Proceed normally from the beginning # Proceed normally from the beginning
@chargeStripe(req, res, payment, product) @chargeStripe(req, res, payment, product)
else if charge and not payment else if charge and not payment
# Initialized Payment. Start from charging. # Initialized Payment. Start from charging.
@recordStripeCharge(req, res, payment, product, charge) @recordStripeCharge(req, res, payment, product, charge)
else else
# Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase. # Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase.
@recalculateGemsFor(req.user, (err) => @recalculateGemsFor(req.user, (err) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@sendPaymentHipChatMessage user: req.user, payment: payment
@sendSuccess(res, @formatEntity(req, payment)) @sendSuccess(res, @formatEntity(req, payment))
) )
) )
) )
chargeStripe: (req, res, payment, product) -> chargeStripe: (req, res, payment, product) ->
stripe.charges.create({ stripe.charges.create({
amount: product.amount amount: product.amount
@ -206,7 +208,7 @@ PaymentHandler = class PaymentHandler extends Handler
}).then( }).then(
# success case # success case
((charge) => @recordStripeCharge(req, res, payment, product, charge)), ((charge) => @recordStripeCharge(req, res, payment, product, charge)),
# error case # error case
((err) => ((err) =>
if err.type in ['StripeCardError', 'StripeInvalidRequestError'] if err.type in ['StripeCardError', 'StripeInvalidRequestError']
@ -214,8 +216,8 @@ PaymentHandler = class PaymentHandler extends Handler
else else
@sendDatabaseError(res, 'Error charging card, please retry.')) @sendDatabaseError(res, 'Error charging card, please retry.'))
) )
recordStripeCharge: (req, res, payment, product, charge) -> recordStripeCharge: (req, res, payment, product, charge) ->
return @sendError(res, 500, 'Fake db error for testing.') if req.body.breakAfterCharging return @sendError(res, 500, 'Fake db error for testing.') if req.body.breakAfterCharging
payment = @makeNewInstance(req) payment = @makeNewInstance(req)
@ -241,9 +243,9 @@ PaymentHandler = class PaymentHandler extends Handler
) )
) )
#- Incrementing/recalculating gems #- Incrementing/recalculating gems
incrementGemsFor: (user, gems, done) -> incrementGemsFor: (user, gems, done) ->
purchased = _.clone(user.get('purchased')) purchased = _.clone(user.get('purchased'))
if not purchased?.gems if not purchased?.gems
@ -251,10 +253,10 @@ PaymentHandler = class PaymentHandler extends Handler
purchased.gems = gems purchased.gems = gems
user.set('purchased', purchased) user.set('purchased', purchased)
user.save((err) -> done(err)) user.save((err) -> done(err))
else else
user.update({$inc: {'purchased.gems': gems}}, {}, (err) -> done(err)) user.update({$inc: {'purchased.gems': gems}}, {}, (err) -> done(err))
recalculateGemsFor: (user, done) -> recalculateGemsFor: (user, done) ->
Payment.find({recipient: user._id}).select('gems').exec((err, payments) -> Payment.find({recipient: user._id}).select('gems').exec((err, payments) ->
@ -264,7 +266,14 @@ PaymentHandler = class PaymentHandler extends Handler
purchased.gems = gems purchased.gems = gems
user.set('purchased', purchased) user.set('purchased', purchased)
user.save((err) -> done(err)) user.save((err) -> done(err))
) )
sendPaymentHipChatMessage: (options) ->
try
message = "#{options.user?.get('name')} bought #{options.payment?.get('amount')} via #{options.payment?.get('service'}."
hipchat.sendHipChatMessage message
catch e
log.error "Couldn't send HipChat message on payment because of error: #{e}"
module.exports = new PaymentHandler() module.exports = new PaymentHandler()

View file

@ -21,6 +21,10 @@ AchievablePlugin = (schema, options) ->
# Check if an achievement has been earned # Check if an achievement has been earned
schema.post 'save', (doc) -> schema.post 'save', (doc) ->
# sometimes post appears to be called twice. Handle this...
# TODO: Refactor this system to make it request-specific,
# perhaps by having POST/PUT requests store the copy on the request object themselves.
return if doc.isInit('_id') and not (doc.id of before)
isNew = not doc.isInit('_id') or not (doc.id of before) isNew = not doc.isInit('_id') or not (doc.id of before)
originalDocObj = before[doc.id] unless isNew originalDocObj = before[doc.id] unless isNew

View file

@ -192,7 +192,8 @@ module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
module.exports.makeNewUser = makeNewUser = (req) -> module.exports.makeNewUser = makeNewUser = (req) ->
user = new User({anonymous: true}) user = new User({anonymous: true})
user.set 'testGroupNumber', Math.floor(Math.random() * 256) # also in app/lib/auth user.set 'testGroupNumber', Math.floor(Math.random() * 256) # also in app/lib/auth
user.set 'preferredLanguage', languages.languageCodeFromAcceptedLanguages req.acceptedLanguages lang = languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
user.set 'preferredLanguage', lang if lang[...2] isnt 'en'
user.set 'lastIP', req.connection.remoteAddress user.set 'lastIP', req.connection.remoteAddress
createMailOptions = (receiver, password) -> createMailOptions = (receiver, password) ->