Merge branch 'achievements' of https://github.com/rubenvereecken/codecombat into rubenvereecken-achievements

This commit is contained in:
Scott Erickson 2014-08-13 10:49:10 -07:00
commit e3088ad813
126 changed files with 3199 additions and 520 deletions

View file

@ -1,12 +0,0 @@
// Fixtures
db.achievements.insert({
query: '{"level.original": "52d97ecd32362bc86e004e87"}',
index: true,
slug: 'dungeon-arena-started',
name: 'Dungeon Arena started',
worth: 1,
collection: 'level.session',
description: 'Started playing Dungeon Arena.',
userField: 'creator'
});

View file

@ -18,9 +18,10 @@ module.exports = class CocoRouter extends Backbone.Router
'about': go('AboutView')
'account/profile(/:userID)': go('account/JobProfileView')
'account': go('account/MainAccountView')
'account/settings': go('account/AccountSettingsView')
'account/unsubscribe': go('account/UnsubscribeView')
#'account/payment'
'admin': go('admin/MainAdminView')
'admin/candidates': go('admin/CandidatesView')
@ -50,7 +51,7 @@ module.exports = class CocoRouter extends Backbone.Router
'editor': go('editor/MainEditorView')
'editor/achievement': go('editor/achievement/AchievementSearchView')
'editor/achievement': go('editor/achievement/AchievementEditView')
'editor/achievement/:articleID': go('editor/achievement/AchievementEditView')
'editor/article': go('editor/article/ArticleSearchView')
'editor/article/preview': go('editor/article/ArticlePreviewView')
'editor/article/:articleID': go('editor/article/ArticleEditView')
@ -79,6 +80,11 @@ module.exports = class CocoRouter extends Backbone.Router
'test(/*subpath)': go('TestView')
'user/:slugOrID': go('user/MainUserView')
'user/:slugOrID/stats': go('user/AchievementsView')
'user/:slugOrID/profile': go('user/JobProfileView')
#'user/:slugOrID/code': go('user/CodeView')
'*name': 'showNotFoundView'
routeToServer: (e) ->

View file

@ -5,7 +5,6 @@ locale = require 'locale/locale'
{me} = require 'lib/auth'
Tracker = require 'lib/Tracker'
CocoView = require 'views/kinds/CocoView'
AchievementNotify = require '../../templates/achievement_notify'
marked.setOptions {gfm: true, sanitize: true, smartLists: true, breaks: false}
@ -40,7 +39,6 @@ Application = initialize: ->
@facebookHandler = new FacebookHandler()
@gplusHandler = new GPlusHandler()
$(document).bind 'keydown', preventBackspace
$.notify.addStyle 'achievement', html: $(AchievementNotify())
@linkedinHandler = new LinkedInHandler()
preload(COMMON_FILES)
$.i18n.init {

View file

@ -0,0 +1,6 @@
CocoCollection = require 'collections/CocoCollection'
Achievement = require 'models/Achievement'
module.exports = class AchievementCollection extends CocoCollection
url: '/db/achievement'
model: Achievement

View file

@ -0,0 +1,9 @@
CocoCollection = require 'collections/CocoCollection'
EarnedAchievement = require 'models/EarnedAchievement'
module.exports = class EarnedAchievementCollection extends CocoCollection
model: EarnedAchievement
initialize: (userID) ->
@url = "/db/user/#{userID}/achievements"
super()

View file

@ -1,6 +1,9 @@
CocoCollection = require 'collections/CocoCollection'
Achievement = require 'models/Achievement'
class NewAchievementCollection extends CocoCollection
model: Achievement
initialize: (me = require('lib/auth').me) ->
@url = "/db/user/#{me.id}/achievements?notified=false"

View file

@ -0,0 +1,9 @@
CocoCollection = require './CocoCollection'
LevelSession = require 'models/LevelSession'
module.exports = class RecentlyPlayedCollection extends CocoCollection
model: LevelSession
constructor: (userID, options) ->
@url = "/db/user/#{userID}/recently_played"
super options

View file

@ -0,0 +1,10 @@
CocoCollection = require 'collections/CocoCollection'
Achievement = require 'models/Achievement'
class RelatedAchievementCollection extends CocoCollection
model: Achievement
initialize: (relatedID) ->
@url = "/db/achievement?related=#{relatedID}"
module.exports = RelatedAchievementCollection

View file

@ -1,6 +1,7 @@
Bus = require './Bus'
{me} = require 'lib/auth'
LevelSession = require 'models/LevelSession'
utils = require 'lib/utils'
module.exports = class LevelBus extends Bus
@ -22,6 +23,7 @@ module.exports = class LevelBus extends Bus
'tome:spell-changed': 'onSpellChanged'
'tome:spell-created': 'onSpellCreated'
'application:idle-changed': 'onIdleChanged'
'goal-manager:new-goal-states': 'onNewGoalStates'
constructor: ->
super(arguments...)
@ -192,6 +194,14 @@ module.exports = class LevelBus extends Bus
@changedSessionProperties.state = true
@saveSession()
onNewGoalStates: ({goalStates})->
state = @session.get 'state'
unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change
state.goalStates = goalStates
@session.set 'state', state
@changedSessionProperties.state = true
@saveSession()
onPlayerJoined: (snapshot) =>
super(arguments...)
return unless @onPoint()

View file

@ -24,6 +24,7 @@ doQuerySelector = (value, operatorObj) ->
matchesQuery = (target, queryObj) ->
return true unless queryObj
throw new Error 'Expected an object to match a query against, instead got null' unless target
for prop, query of queryObj
if prop[0] == '$'
switch prop

View file

@ -1,4 +1,4 @@
CocoClass = require 'lib/CocoClass'
CocoClass = require './CocoClass'
namesCache = {}

View file

@ -1,4 +1,4 @@
SystemNameLoader = require 'lib/SystemNameLoader'
SystemNameLoader = require './SystemNameLoader'
###
Good-to-knows:
dataPath: an array of keys that walks you up a JSON object that's being patched
@ -12,7 +12,7 @@ module.exports.expandDelta = (delta, left, schema) ->
(expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas)
flattenDelta = (delta, dataPath=null, deltaPath=null) ->
module.exports.flattenDelta = flattenDelta = (delta, dataPath=null, deltaPath=null) ->
# takes a single jsondiffpatch delta and returns an array of objects with
return [] unless delta
dataPath ?= []
@ -175,3 +175,5 @@ prunePath = (delta, path) ->
prunePath delta[path[0]], path.slice(1) unless delta[path[0]] is undefined
keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t')
delete delta[path[0]] if keys.length is 0

View file

@ -70,6 +70,7 @@ module.exports.i18n = (say, target, language=me.lang(), fallback='en') ->
null
module.exports.getByPath = (target, path) ->
throw new Error 'Expected an object to match a query against, instead got null' unless target
pieces = path.split('.')
obj = target
for piece in pieces
@ -82,7 +83,7 @@ module.exports.isID = (id) -> _.isString(id) and id.length is 24 and id.match(/[
module.exports.round = _.curry (digits, n) ->
n = +n.toFixed(digits)
positify = (func) -> (x) -> if x > 0 then func(x) else 0
positify = (func) -> (params) -> (x) -> if x > 0 then func(params)(x) else 0
# f(x) = ax + b
createLinearFunc = (params) ->
@ -100,3 +101,32 @@ module.exports.functionCreators =
linear: positify(createLinearFunc)
quadratic: positify(createQuadraticFunc)
logarithmic: positify(createLogFunc)
# Call done with true to satisfy the 'until' goal and stop repeating func
module.exports.keepDoingUntil = (func, wait=100, totalWait=5000) ->
waitSoFar = 0
(done = (success) ->
if (waitSoFar += wait) <= totalWait and not success
_.delay (-> func done), wait) false
module.exports.grayscale = (imageData) ->
d = imageData.data
for i in [0..d.length] by 4
r = d[i]
g = d[i+1]
b = d[i+2]
v = 0.2126*r + 0.7152*g + 0.0722*b
d[i] = d[i+1] = d[i+2] = v
imageData
# Deep compares l with r, with the exception that undefined values are considered equal to missing values
# Very practical for comparing Mongoose documents where undefined is not allowed, instead fields get deleted
module.exports.kindaEqual = compare = (l, r) ->
if _.isObject(l) and _.isObject(r)
for key in _.union Object.keys(l), Object.keys(r)
return false unless compare l[key], r[key]
return true
else if l is r
return true
else
return false

View file

@ -49,6 +49,9 @@
blog: "Blog"
forum: "Forum"
account: "Account"
profile: "Profile"
stats: "Stats"
code: "Code"
admin: "Admin"
home: "Home"
contribute: "Contribute"
@ -176,12 +179,14 @@
new_password: "New Password"
new_password_verify: "Verify"
email_subscriptions: "Email Subscriptions"
email_subscriptions_none: "No Email Subscriptions."
email_announcements: "Announcements"
email_announcements_description: "Get emails on the latest news and developments at CodeCombat."
email_notifications: "Notifications"
email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
email_any_notes: "Any Notifications"
email_any_notes_description: "Disable to stop all activity notification emails."
email_news: "News"
email_recruit_notes: "Job Opportunities"
email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
contributor_emails: "Contributor Class Emails"
@ -591,6 +596,10 @@
level_search_title: "Search Levels Here"
achievement_search_title: "Search Achievements"
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"
achievement_query_goals: "Key achievement off of level goals"
level_completion: "Level Completion"
article:
edit_btn_preview: "Preview"
@ -599,6 +608,7 @@
general:
and: "and"
name: "Name"
date: "Date"
body: "Body"
version: "Version"
commit_msg: "Commit Message"
@ -938,3 +948,38 @@
text_diff: "Text Diff"
merge_conflict_with: "MERGE CONFLICT WITH"
no_changes: "No Changes"
user:
stats: "Stats"
singleplayer_title: "Singleplayer Levels"
multiplayer_title: "Multiplayer Levels"
achievements_title: "Achievements"
last_played: "Last Played"
status: "Status"
status_completed: "Completed"
status_unfinished: "Unfinished"
no_singleplayer: "No Singleplayer games played yet."
no_multiplayer: "No Multiplayer games played yet."
no_achievements: "No Achievements earned yet."
favorite_prefix: "Favorite language is "
favorite_postfix: "."
achievements:
last_earned: "Last Earned"
amount_achieved: "Amount"
achievement: "Achievement"
category_contributor: "Contributor"
category_miscellaneous: "Miscellaneous"
category_levels: "Levels"
category_undefined: "Uncategorized"
current_xp_prefix: ""
current_xp_postfix: " in total"
new_xp_prefix: ""
new_xp_postfix: " earned"
left_xp_prefix: ""
left_xp_infix: " until level "
left_xp_postfix: ""
account:
recently_played: "Recently Played"
no_recent_games: "No games played during the past two weeks."

View file

@ -1,5 +1,5 @@
CocoModel = require './CocoModel'
util = require '../lib/utils'
utils = require '../lib/utils'
module.exports = class Achievement extends CocoModel
@className: 'Achievement'
@ -11,6 +11,47 @@ module.exports = class Achievement extends CocoModel
# TODO logic is duplicated in Mongoose Achievement schema
getExpFunction: ->
kind = @get('function')?.kind or @schema.function.default.kind
parameters = @get('function')?.parameters or @schema.function.default.parameters
kind = @get('function')?.kind or jsonschema.properties.function.default.kind
parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
@styleMapping:
1: 'achievement-wood'
2: 'achievement-stone'
3: 'achievement-silver'
4: 'achievement-gold'
5: 'achievement-diamond'
getStyle: -> Achievement.styleMapping[@get 'difficulty']
@defaultImageURL: '/images/achievements/default.png'
getImageURL: ->
if @get 'icon' then '/file/' + @get('icon') else Achievement.defaultImageURL
hasImage: -> @get('icon')?
# TODO Could cache the default icon separately
cacheLockedImage: ->
return @lockedImageURL if @lockedImageURL
canvas = document.createElement 'canvas'
image = new Image
image.src = @getImageURL()
defer = $.Deferred()
image.onload = =>
canvas.width = image.width
canvas.height = image.height
context = canvas.getContext '2d'
context.drawImage image, 0, 0
imgData = context.getImageData 0, 0, canvas.width, canvas.height
imgData = utils.grayscale imgData
context.putImageData imgData, 0, 0
@lockedImageURL = canvas.toDataURL()
defer.resolve @lockedImageURL
defer
getLockedImageURL: -> @lockedImageURL
i18nName: -> utils.i18n @attributes, 'name'
i18nDescription: -> utils.i18n @attributes, 'description'

View file

@ -1,8 +1,6 @@
storage = require 'lib/storage'
deltasLib = require 'lib/deltas'
NewAchievementCollection = require '../collections/NewAchievementCollection'
class CocoModel extends Backbone.Model
idAttribute: '_id'
loaded: false
@ -301,11 +299,13 @@ class CocoModel extends Backbone.Model
return if _.isString @url then @url else @url()
@pollAchievements: ->
NewAchievementCollection = require '../collections/NewAchievementCollection' # Nasty mutual inclusion if put on top
achievements = new NewAchievementCollection
achievements.fetch(
achievements.fetch
success: (collection) ->
me.fetch (success: -> Backbone.Mediator.publish('achievements:new', collection)) unless _.isEmpty(collection.models)
)
error: ->
console.error 'Miserably failed to fetch unnotified achievements', arguments
CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500

View file

@ -0,0 +1,7 @@
CocoModel = require './CocoModel'
utils = require '../lib/utils'
module.exports = class EarnedAchievement extends CocoModel
@className: 'EarnedAchievement'
@schema: require 'schemas/models/earned_achievement'
urlRoot: '/db/earnedachievement'

View file

@ -36,3 +36,9 @@ module.exports = class LevelSession extends CocoModel
spell = item[1]
return true if c1[thang][spell] isnt c2[thang]?[spell]
false
isMultiplayer: ->
@get('team')? # Only multiplayer level sessions have teams defined
completed: ->
@get('state')?.complete || false

View file

@ -58,9 +58,15 @@ module.exports = class SuperModel extends Backbone.Model
return res
else
@addCollection collection
@listenToOnce collection, 'sync', (c) ->
console.debug 'Registering collection', url
@registerCollection c
onCollectionSynced = (c) ->
if collection.url is c.url
console.debug 'Registering collection', url, c
@registerCollection c
else
console.warn 'Sync triggered for collection', c
console.warn 'Yet got other object', c
@listenToOnce collection, 'sync', onCollectionSynced
@listenToOnce collection, 'sync', onCollectionSynced
res = @addModelResource(collection, name, fetchOptions, value)
res.load() if not (res.isLoading or res.isLoaded)
return res

View file

@ -9,14 +9,24 @@ module.exports = class User extends CocoModel
urlRoot: '/db/user'
notyErrors: false
defaults:
points: 0
initialize: ->
super()
@migrateEmails()
onLoaded: ->
CocoModel.pollAchievements() # Check for achievements on login
super arguments...
isAdmin: ->
permissions = @attributes['permissions'] or []
return 'admin' in permissions
isAnonymous: ->
@get 'anonymous'
displayName: ->
@get('name') or 'Anoner'
@ -32,47 +42,13 @@ module.exports = class User extends CocoModel
return "/file/#{photoURL}#{prefix}s=#{size}"
return "/db/user/#{@id}/avatar?s=#{size}&employerPageAvatar=#{useEmployerPageAvatar}"
@getByID = (id, properties, force) ->
{me} = require 'lib/auth'
return me if me.id is id
user = cache[id] or new module.exports({_id: id})
if force or not cache[id]
user.loading = true
user.fetch(
success: ->
user.loading = false
Backbone.Mediator.publish('user:fetched')
#user.trigger 'sync' # needed?
)
cache[id] = user
user
getSlugOrID: -> @get('slug') or @get('_id')
set: ->
if arguments[0] is 'jobProfileApproved' and @get("jobProfileApproved") is false and not @get("jobProfileApprovedDate")
@set "jobProfileApprovedDate", (new Date()).toISOString()
super arguments...
# callbacks can be either success or error
@getByIDOrSlug: (idOrSlug, force, callbacks={}) ->
{me} = require 'lib/auth'
isID = util.isID idOrSlug
if me.id is idOrSlug or me.slug is idOrSlug
callbacks.success me if callbacks.success?
return me
cached = cache[idOrSlug]
user = cached or new @ _id: idOrSlug
if force or not cached
user.loading = true
user.fetch
success: ->
user.loading = false
Backbone.Mediator.publish 'user:fetched'
callbacks.success user if callbacks.success?
error: ->
user.loading = false
callbacks.error user if callbacks.error?
cache[idOrSlug] = user
user
@getUnconflictedName: (name, done) ->
$.ajax "/auth/name/#{name}",
success: (data) -> done data.name
@ -111,19 +87,16 @@ module.exports = class User extends CocoModel
isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled
a = 5
b = 40
b = 100
c = b
# y = a * ln(1/b * (x + b)) + 1
# y = a * ln(1/b * (x + c)) + 1
@levelFromExp: (xp) ->
if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + b))) + 1 else 1
if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + c))) + 1 else 1
# x = (e^((y-1)/a) - 1) * b
# x = b * e^((y-1)/a) - c
@expForLevel: (level) ->
Math.ceil((Math.exp((level - 1)/ a) - 1) * b)
if level > 1 then Math.ceil Math.exp((level - 1)/ a) * b - c else 0
level: ->
User.levelFromExp(@get('points'))
levelFromExp: (xp) -> User.levelFromExp(xp)
expForLevel: (level) -> User.expForLevel(level)

View file

@ -24,8 +24,9 @@ MongoFindQuerySchema =
'^[-a-zA-Z0-9\.]*$':
oneOf: [
#{$ref: '#/definitions/' + MongoQueryOperatorSchema.id},
{type: 'string'},
{type: 'string'}
{type: 'object'}
{type: 'boolean'}
]
additionalProperties: true # TODO make Treema accept new pattern matched keys
definitions: {}
@ -34,36 +35,58 @@ MongoFindQuerySchema.definitions[MongoQueryOperatorSchema.id] = MongoQueryOperat
AchievementSchema = c.object()
c.extendNamedProperties AchievementSchema
c.extendBasicProperties AchievementSchema, 'article'
c.extendBasicProperties AchievementSchema, 'achievement'
c.extendSearchableProperties AchievementSchema
_.extend(AchievementSchema.properties,
_.extend AchievementSchema.properties,
query:
#type:'object'
$ref: '#/definitions/' + MongoFindQuerySchema.id
worth: {type: 'number'}
worth: c.float
default: 10
collection: {type: 'string'}
description: {type: 'string'}
userField: {type: 'string'}
description: c.shortString
default: 'Probably the coolest you\'ll ever get.'
userField: c.shortString()
related: c.objectId(description: 'Related entity')
icon: {type: 'string', format: 'image-file', title: 'Icon'}
category:
enum: ['level', 'ladder', 'contributor']
description: 'For categorizing and display purposes'
difficulty: c.int
description: 'The higher the more difficult'
default: 1
proportionalTo:
type: 'string'
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
recalculable:
type: 'boolean'
description: 'Needs to be set to true before it is elligible for recalculation.'
default: true
function:
type: 'object'
description: 'Function that gives total experience for X amount achieved'
properties:
kind: {enum: ['linear', 'logarithmic'], default: 'linear'}
kind: {enum: ['linear', 'logarithmic', 'quadratic'], default: 'linear'}
parameters:
type: 'object'
properties:
a: {type: 'number', default: 1}
b: {type: 'number', default: 1}
c: {type: 'number', default: 1}
additionalProperties: true
default: {kind: 'linear', parameters: a: 1}
required: ['kind', 'parameters']
additionalProperties: false
)
i18n: c.object
format: 'i18n'
props: ['name', 'description']
description: 'Help translate this achievement'
_.extend AchievementSchema, # Let's have these on the bottom
# TODO We really need some required properties in my opinion but this makes creating new achievements impossible as it is now
#required: ['name', 'description', 'query', 'worth', 'collection', 'userField', 'category', 'difficulty']
additionalProperties: false
AchievementSchema.definitions = {}
AchievementSchema.definitions[MongoFindQuerySchema.id] = MongoFindQuerySchema

View file

@ -101,6 +101,14 @@ _.extend LevelSessionSchema.properties,
type: 'object'
source:
type: 'string'
goalStates:
type: 'object'
description: 'Maps Goal ID on a goal state object'
additionalProperties:
title: 'Goal State'
type: 'object'
properties:
status: enum: ['failure', 'incomplete', 'success']
code:
type: 'object'

View file

@ -20,6 +20,9 @@ PatchSchema = c.object({title: 'Patch', required: ['target', 'delta', 'commitMes
major: {type: 'number', minimum: 0}
minor: {type: 'number', minimum: 0}
})
wasPending: type: 'boolean'
newlyAccepted: type: 'boolean'
})
c.extendBasicProperties(PatchSchema, 'patch')

View file

@ -222,6 +222,33 @@ _.extend UserSchema.properties,
points: {type: 'number'}
activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity}
stats: c.object {additionalProperties: false},
gamesCompleted: c.int()
articleEdits: c.int()
levelEdits: c.int()
levelSystemEdits: c.int()
levelComponentEdits: c.int()
thangTypeEdits: c.int()
patchesSubmitted: c.int
description: 'Amount of patches submitted, not necessarily accepted'
patchesContributed: c.int
description: 'Amount of patches submitted and accepted'
patchesAccepted: c.int
description: 'Amount of patches accepted by the user as owner'
# The below patches only apply to those that actually got accepted
totalTranslationPatches: c.int()
totalMiscPatches: c.int()
articleTranslationPatches: c.int()
articleMiscPatches: c.int()
levelTranslationPatches: c.int()
levelMiscPatches: c.int()
levelComponentTranslationPatches: c.int()
levelComponentMiscPatches: c.int()
levelSystemTranslationPatches: c.int()
levelSystemMiscPatches: c.int()
thangTypeTranslationPatches: c.int()
thangTypeMiscPatches: c.int()
c.extendBasicProperties UserSchema, 'user'

View file

@ -19,6 +19,8 @@ me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ex
# 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.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext)
me.int = (ext) -> combine {type: 'integer'}, ext
me.float = (ext) -> combine {type: 'number'}, ext
PointSchema = me.object {title: 'Point', description: 'An {x, y} coordinate point.', format: 'point2d', required: ['x', 'y']},
x: {title: 'x', description: 'The x coordinate.', type: 'number', 'default': 15}

View file

@ -0,0 +1,32 @@
@import "../bootstrap/variables"
@import "../bootstrap/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,243 @@
@import 'bootstrap/variables'
.achievement-body
position: relative
.achievement-icon
position: absolute
.achievement-image
width: 100%
height: 100%
img
position: absolute
margin: auto
top: 0
left: 0
right: 0
bottom: 0
&.locked
.achievement-content
background-image: url("/images/achievements/achievement_background_locked.png")
&:not(.locked)
.achievement-content
background-image: url("/images/achievements/achievement_background_light.png")
.achievement-content
background-size: 100% 100%
text-align: center
overflow: hidden
> .achievement-title
font-family: $font-family-base
font-weight: bold
white-space: nowrap
max-height: 2em
overflow: hidden
text-overflow: ellipsis
> .achievement-description
white-space: initial
font-size: 12px
line-height: 1.3em
max-height: 2.6em
margin-top: auto
margin-bottom: 0px !important
padding-left: 5px
overflow: hidden
text-overflow: ellipsis
// Specific to the user stats page
#user-achievements-view
.achievement-body
width: 335px
height: 120px
margin: 10px 0px
.achievement-icon
width: 120px
height: 120px
top: -10px
.achievement-image
img
-moz-transform: scale(0.6)
-webkit-transform: scale(0.6)
transform: scale(0.6)
.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
padding: 20px 0px
position: relative
.achievement-body
.achievement-icon
z-index: 1000
width: 200px
height: 200px
left: -140px
top: -20px
.achievement-image
img
position: absolute
margin: auto
top: 0
left: 0
right: 0
bottom: 0
.achievement-content
background-image: url("/images/achievements/achievement_background.png")
position: relative
width: 450px
height: 160px
padding: 24px 30px 20px 60px
.achievement-title
font-family: Bangers
font-size: 28px
padding-left: -50px
.achievement-description
font-size: 15px
line-height: 1.3em
max-height: 2.6em
margin-top: auto
margin-bottom: 0px !important
.progress-wrapper
margin-left: 20px
position: absolute
bottom: 48px
.user-level
font-size: 20px
color: white
position: absolute
left: -15px
margin-top: -8px
vertical-align: middle
z-index: 1000
> .progress-bar-wrapper
position: absolute
margin-left: 17px
width: 314px
height: 20px
z-index: 2
> .progress
margin-top: 5px
border-radius: 50px
height: 14px
> .progress-bar-border
position: absolute
width: 340px
height: 30px
margin-top: -2px
background: url("/images/achievements/bar_border.png") no-repeat
background-size: 100% 100%
z-index: 1
.achievement-icon
background-size: 100% 100% !important
.achievement-wood
&.locked
.achievement-icon
background: url("/images/achievements/border_wood_locked.png") no-repeat
&:not(.locked)
.achievement-icon
background: url("/images/achievements/border_wood.png") no-repeat
.achievement-stone
&.locked
.achievement-icon
background: url("/images/achievements/border_stone_locked.png") no-repeat
&:not(.locked)
.achievement-icon
background: url("/images/achievements/border_stone.png") no-repeat
.achievement-silver
&.locked
.achievement-icon
background: url("/images/achievements/border_silver_locked.png") no-repeat
&:not(.locked)
.achievement-icon
background: url("/images/achievements/border_silver.png") no-repeat
.achievement-gold
&.locked
.achievement-icon
background: url("/images/achievements/border_gold_locked.png") no-repeat
&:not(.locked)
.achievement-icon
background: url("/images/achievements/border_gold.png") no-repeat
.achievement-diamond
&.locked
.achievement-icon
background: url("/images/achievements/border_diamond_locked.png") no-repeat
&:not(.locked)
.achievement-icon
background: url("/images/achievements/border_diamond.png") no-repeat
.xp-bar-old
background-color: #680080
.xp-bar-new
background-color: #0096ff
.xp-bar-left
background-color: #fffbfd
// Achievements page
.achievement-category-title
margin-left: 20px
font-family: $font-family-base
font-weight: bold
color: #5a5a5a
text-transform: uppercase
.table-layout
#no-achievements
margin-top: 40px
.achievement-icon-small
height: 18px
// Achievement Popup
.achievement-popup-container
position: fixed
right: 0px
bottom: 0px
.popup
cursor: default
left: 600px
.user-level
background-image: url("/images/achievements/level-bg.png")
width: 38px
height: 38px
line-height: 38px
font-size: 20px
font-family: $font-family-base

View file

@ -21,7 +21,8 @@ h1 h2 h3 h4
margin: 56px auto 0
min-height: 600px
padding: 14px 12px 5px 12px
@include box-sizing(border-box)
+box-sizing(border-box)
+clearfix()
#outer-content-wrapper
background: #B4B4B4
@ -291,3 +292,9 @@ body[lang='ja']
a[data-toggle="coco-modal"]
cursor: pointer
.achievement-corner
position: fixed
bottom: 0px
right: 0px
z-index: 1001

View file

@ -1,4 +1,44 @@
@import "../bootstrap/variables"
@import "../bootstrap/mixins"
// This is still very blocky. Browser reflows? Investigate why.
.open > .dropdown-menu
animation-name: fadeAnimation
animation-duration: .7s
animation-iteration-count: 1
animation-timing-function: ease
animation-fill-mode: forwards
-webkit-animation-name: fadeAnimation
-webkit-animation-duration: .7s
-webkit-animation-iteration-count: 1
-webkit-animation-timing-function: ease
-webkit-animation-fill-mode: backwards
-moz-animation-name: fadeAnimation
-moz-animation-duration: .7s
-moz-animation-iteration-count: 1
-moz-animation-timing-function: ease
-moz-animation-fill-mode: forwards
@keyframes fadeAnimation
from
opacity: 0
top: 120%
to
opacity: 1
top: 100%
@-webkit-keyframes fadeAnimation
from
opacity: 0
top: 120%
to
opacity: 1
top: 100%
a.disabled
color: #5b5855
text-decoration: none
cursor: default
#top-nav
a.navbar-brand
@ -19,11 +59,65 @@
.account-settings-image
width: 18px
height: 18px
margin-right: 5px
.glyphicon-user
font-size: 16px
margin-right: 5px
.nav.navbar-link-text, .nav.navbar-link-text > li > a
.dropdown
.dropdown-menu
left: auto
width: 280px
padding: 0px
border-radius: 0px
font-family: Bangers
> .user-dropdown-header
position: relative
background: #E4CF8C
height: 160px
padding: 10px
text-align: center
color: black
border-bottom: #32281e 1px solid
> a:hover
background-color: transparent
img
border: #e3be7a 8px solid
height: 98px // Includes the border
&:hover
box-shadow: 0 0 20px #e3be7a
> h3
margin-top: 10px
text-shadow: 2px 2px 3px white
color: #31281E
.user-level
position: absolute
top: 73px
right: 86px
color: gold
text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black
.user-dropdown-body
color: black
padding: 15px
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
.nav.navbar-link-text > li > a
font-weight: normal
font-size: 25px
letter-spacing: 2px
@ -31,7 +125,7 @@
&:hover
color: #f8e413
.navbar-link-text a:hover
.navbar-link-text > li > a:hover
background: darken($body-bg, 3%)
.btn, .btn-group, .fancy-select
@ -67,9 +161,6 @@
top: 13px
max-width: 140px
.nav
margin-bottom: 0
div.fancy-select
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25)
div.trigger

View file

@ -1,4 +1,6 @@
#editor-achievement-edit-view
height: 100%
.treema-root
margin: 28px 0px 20px
@ -10,3 +12,9 @@
textarea
width: 92%
height: 300px
#achievement-view
min-height: 200px
position: relative
padding-left: 200px

View file

@ -0,0 +1,6 @@
#related-achievements-view
#new-achievement-button
margin-bottom: 10px
.icon-column
width: 25px

View file

@ -1,50 +0,0 @@
.notifyjs-achievement-base
//background: url("/images/pages/base/notify_mockup.png")
background-image: url("/images/pages/base/modal_background.png")
background-size: 100% 100%
width: 500px
height: 200px
padding: 35px 35px 15px 15px
text-align: center
cursor: auto
.achievement-body
.achievement-image
img
float: left
width: 100px
height: 100px
border-radius: 50%
margin: 20px 30px 20px 30px
-webkit-box-shadow: 0px 0px 36px 0px white
-moz-box-shadow: 0px 0px 36px 0px white
box-shadow: 0px 0px 36px 0px white
.achievement-title
font-family: Bangers
font-size: 28px
.achievement-description
margin-top: 10px
font-size: 16px
.achievement-progress
padding: 15px 0px 0px 0px
.achievement-message
font-family: Bangers
font-size: 18px
&:empty
display: none
.progress-wrapper
.progress-bar-wrapper
width: 100%
.earned-exp
padding-left: 5px
font-family: Bangers
font-size: 16px
float: right
.progress-bar-white
background-color: white

View file

@ -0,0 +1,73 @@
@import "../bootstrap/variables"
@import "../bootstrap/mixins"
#user-home
margin-top: 20px
.left-column
+make-sm-column(4)
.right-column
+make-sm-column(8)
.profile-wrapper
text-align: center
outline: 1px solid darkgrey
max-width: 100%
+center-block()
> .picture
width: 100%
background-color: #ffe4bc
border: 4px solid white
> .profile-info
background: white
.extra-info
padding-bottom: 3px
&:empty
display: none
.name
margin: 0px auto
padding: 10px inherit
color: white
text-shadow: 2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000, 1px 1px #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000
.profile-menu
padding-left: 0px
width: 100%
> a
border-radius: 0
border-width: 1px 0px 0px 0px
border-color: darkgrey
&:hover
border-color: #888
> span
color: #555555
font-size: 15px
margin-left: 5px
.contributor-categories
list-style: none
padding: 0px
margin-top: 15px
> .contributor-category
outline: 1px solid black
margin-bottom: 15px
> .contributor-image
border: none
width: 100%
border-bottom: 1px solid black
> .contributor-title
text-align: center
padding: 5px 0px
margin: 0px
background: white
.vertical-buffer
padding: 10px 0px

View file

@ -0,0 +1,141 @@
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

@ -1,12 +0,0 @@
div
.clearfix.achievement-body
.achievement-image(data-notify-html="image")
.achievement-content
.achievement-title(data-notify-html="title")
.achievement-description(data-notify-html="description")
.achievement-progress
.achievement-message(data-notify-html="message")
.progress-wrapper
.earned-exp(data-notify-html="earnedExp")
.progress-bar-wrapper(data-notify-html="progressBar")

View file

@ -0,0 +1,21 @@
- var addedClass = style + (locked === true ? ' locked' : '')
.clearfix.achievement-body(class=addedClass)
.achievement-icon
.achievement-image
img(src=imgURL)
.achievement-content
.achievement-title= title
p.achievement-description= description
if popup
.progress-wrapper
span.user-level= level
.progress-bar-wrapper
.progress
- var currentTitle = $.i18n.t('achievements.current_xp_prefix') + currentXP + ' XP' + $.i18n.t('achievements.current_xp_postfix');
- var newTitle = $.i18n.t('achievements.new_xp_prefix') + newXP + ' XP' + $.i18n.t('achievements.new_xp_postfix');
- var leftTitle = $.i18n.t('achievements.left_xp_prefix') + newXP + ' XP' + $.i18n.t('achievements.left_xp_infix') + (level+1) + $.i18n.t('achievements.left_xp_postfix');
.progress-bar.xp-bar-old(style="width:#{oldXPWidth}%" data-toggle="tooltip" data-placement="top" title="#{currentTitle}")
.progress-bar.xp-bar-new(style="width:#{newXPWidth}%" data-toggle="tooltip" title="#{newTitle}")
.progress-bar.xp-bar-left(style="width:#{leftXPWidth}%" data-toggle="tooltip" title="#{leftTitle}")
.progress-bar-border

View file

@ -27,26 +27,44 @@ body
select.language-dropdown
if me.get('anonymous') === false
button.btn.btn-primary.navbuttontext.header-font#logout-button(data-i18n="login.log_out") Log Out
a.btn.btn-primary.navbuttontext.header-font(href=me.get('jobProfile') ? "/account/profile/#{me.id}" : "/account/settings")
div.navbuttontext-account(data-i18n="nav.account") Account
if me.get('photoURL')
img.account-settings-image(src=me.getPhotoURL(18), alt="")
else
span.glyphicon.glyphicon-user
else
button.btn.btn-primary.navbuttontext.header-font.auth-button
span(data-i18n="login.log_in") Log In
span.spr.spl /
span(data-i18n="login.sign_up") Create Account
ul(class='navbar-link-text').nav.navbar-nav.pull-right
li.play
a.header-font(href='/play', data-i18n="nav.play") Levels
li
a.header-font(href='/community', data-i18n="nav.community") Community
if me.get('anonymous') === false
li.dropdown
button.btn.btn-primary.navbuttontext.header-font.dropdown-toggle(href="#", data-toggle="dropdown")
if me.get('photoURL')
img.account-settings-image(src=me.getPhotoURL(18), alt="")
else
i.glyphicon.glyphicon-user
.navbuttontext-account(data-i18n="nav.account" href="/account") Account
span.caret
ul.dropdown-menu(role="menu")
li.user-dropdown-header
span.user-level= me.level()
a(href="/user/#{me.getSlugOrID()}")
img.img-circle(src="#{me.getPhotoURL()}" alt="")
h3=me.displayName()
li.user-dropdown-body
.col-xs-4.text-center
a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile") Profile
.col-xs-4.text-center
a(href="/user/#{me.getSlugOrID()}/stats" data-i18n="nav.stats") Stats
.col-xs-4.text-center
a.disabled(data-i18n="nav.code") Code
li.user-dropdown-footer
.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
li
button.btn.btn-primary.navbuttontext.header-font.auth-button
span(data-i18n="login.log_in") Log In
span.spr.spl /
span(data-i18n="login.sign_up") Create Account
block outer_content
#outer-content-wrapper(class=showBackground ? 'show-background' : '')
@ -55,6 +73,7 @@ body
.main-content-area
block content
p If this is showing, you dun goofed
.achievement-corner
block footer
.footer.clearfix

View file

@ -2,16 +2,16 @@ extends /templates/base
block content
if me.isAdmin()
div
ol.breadcrumb
li
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
li
a(href="/editor/achievement", data-i18n="editor.achievement_title") Achievement Editor
li.active
| #{achievement.attributes.name}
ol.breadcrumb
li
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
li
a(href="/editor/achievement", data-i18n="editor.achievement_title") Achievement Editor
li.active
| #{achievement.attributes.name}
button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate
button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete
button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
@ -20,12 +20,10 @@ block content
#achievement-treema
#achievement-view
#achievement-view.clearfix
hr
div#error-view
else
.alert.alert-danger
span Admin only. Turn around.

View file

@ -33,6 +33,8 @@ block header
- var patches = level.get('patches')
if patches && patches.length
span.badge= patches.length
li
a(href="#related-achievements-view", data-toggle="tab") Achievements
li
a(href="#docs-components-view", data-toggle="tab", data-i18n="editor.level_tab_docs") Documentation
.navbar-header
@ -121,8 +123,10 @@ block outer_content
div.tab-pane#editor-level-patches
.patches-view
div.tab-pane#related-achievements-view
div.tab-pane#docs-components-view
div#error-view
block footer
block footer

View file

@ -0,0 +1,23 @@
extends /templates/modal/new_model
block modal-body-content
form.form
.form-group
label.control-label(for="name", data-i18n="general.name") Name
input#name.form-control(name="name", type="text")
.form-group
label.control-label(for="description" data-i18n="general.description") Description
input#description.form-control(name="description", type="text")
h4(data-i18n="editor.achievement_query_misc") Key achievement off of miscellanea
.radio
label
input(type="checkbox", name="queryOptions" id="misc-level-completion" value="misc-level-completion")
span.spl(data-i18n="editor.level_completion") Level Completion
- var goals = level.get('goals');
if goals && goals.length
h4(data-i18n="editor.achievement_query_goals") Key achievement off of level goals
each goal in goals
.radio
label
input(type="checkbox", name="queryOptions" id="#{goal.id}" value="#{goal.id}")
span.spl= goal.name

View file

@ -0,0 +1,26 @@
button.btn.btn-primary#new-achievement-button(disabled=me.isAdmin() === true ? undefined : "true" data-i18n="editor.new_achievement_title") Create a New Achievement
if achievements.loading
h2(data-i18n="common.loading") Loading...
else if ! achievements.models.length
.panel
.panel-body
p(data-i18n="editor.no_achievements") No achievements added for this level yet.
else
table.table.table-hover
thead
tr
th
th(data-i18n="general.name") Name
th(data-i18n="general.description") Description
th XP
tbody
each achievement in achievements.models
tr
td(style="width: 20px")
img.achievement-icon-small(src=achievement.getImageURL() alt="#{achievement.get('name') icon")
td
a(href="/editor/achievement/#{achievement.get('slug')}")= achievement.get('name')
td= achievement.get('description')
td= achievement.get('worth')

View file

@ -12,32 +12,12 @@ block content
if me.get('anonymous')
a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="modal/AuthModal", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Content
else
a.btn.btn-primary.open-modal-button(href='#new-model-modal', role="button", data-toggle="modal", data-i18n="#{currentNew}") Create a New Something
a.btn.btn-primary.open-modal-button#new-model-button(data-i18n="#{currentNew}") Create a New Something
input#search(data-i18n="[placeholder]#{currentSearch}")
hr
div.results
table
// TODO: make this into a ModalView subview
div.modal.fade#new-model-modal
.modal-dialog
.background-wrapper
.modal-content
.modal-header
h3(data-i18n="#{currentNew}") Create New #{modelLabel}
.modal-body
form.form
.form-group
label.control-label(for="name", data-i18n="general.name") Name
input#name.form-control(name="name", type="text")
.modal-footer
button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel
button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create
.modal-body.wait.secret
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
.progress.progress-striped.active
.progress-bar
else
.alert.alert-danger
span Admin only. Turn around.

View file

@ -0,0 +1,13 @@
extends /templates/base
// User pages might have some user page specific header, if not remove this
block content
.clearfix
if user && viewName
ol.breadcrumb
li
a(href="/user/#{user.getSlugOrID()}") #{user.displayName()}
li.active
| #{viewName}
if !user || user.loading
| LOADING

View file

@ -0,0 +1,19 @@
extends /templates/modal/modal_base
block modal-header-content
h3(data-i18n="#{currentNew}") Create New #{modelLabel}
block modal-body-content
form.form
.form-group
label.control-label(for="name", data-i18n="general.name") Name
input#name.form-control(name="name", type="text")
block modal-footer
.modal-footer
button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel
button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create
.modal-body.wait.secret
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
.progress.progress-striped.active
.progress-bar

View file

@ -0,0 +1,52 @@
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="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');
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

@ -0,0 +1,133 @@
extends /templates/kinds/user
block append content
if user
.vertical-buffer
.row
.left-column
.profile-wrapper
img.picture(src="#{user.getPhotoURL(150)}" alt="")
div.profile-info
h3.name= user.get('name')
if favoriteLanguage
div.extra-info
span(data-i18n="user.favorite_prefix") Favorite language is
strong.favorite-language= favoriteLanguage
span(data-i18n="user.favorite_postfix") .
.btn-group-vertical.profile-menu
a.btn.btn-default(href="/user/#{user.getSlugOrID()}/profile")
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
ul.contributor-categories
//li.contributor-category
img.contributor-image(src="/images/pages/user/general.png")
h4.contributor-title CodeCombateer
if emails.adventurerNews
li.contributor-category
img.contributor-image(src="/images/pages/user/adventurer.png")
h4.contributor-title
a(href="/contribute#adventurer" data-i18n="classes.adventurer_title") Adventurer
if emails.ambassadorNews
li.contributor-category
img.contributor-image(src="/images/pages/user/ambassador.png")
h4.contributor-title
a(href="/contribute#ambassador" data-i18n="classes.ambassador_title") Ambassador
if emails.archmageNews
li.contributor-category
img.contributor-image(src="/images/pages/user/archmage.png")
h4.contributor-title
a(href="/contribute#archmage" data-i18n="classes.archmage_title") Archmage
if emails.artisanNews
li.contributor-category
img.contributor-image(src="/images/pages/user/artisan.png")
h4.contributor-title
a(href="/contribute#artisan" data-i18n="classes.artisan_title") Artisan
if emails.scribeNews
li.contributor-category
img.contributor-image(src="/images/pages/user/scribe.png")
h4.contributor-title
a(href="/contribute#scribe" data-i18n="classes.scribe_title") Scribe
.right-column
.panel.panel-default
.panel-heading
h3.panel-title(data-i18n="user.singleplayer_title") Singleplayer Levels
if (!singlePlayerSessions)
.panel-body
p(data-i18n="common.loading") Loading...
else if (singlePlayerSessions.length)
table.table
tr
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.status") Status
each session in singlePlayerSessions
if session.get('levelName')
tr
td
a(href="/play/level/#{session.get('levelID')}")= session.get('levelName')
td= moment(session.get('changed')).fromNow()
if session.get('state').complete === true
td(data-i18n="user.status_completed") Completed
else
td(data-i18n="user.status_unfinished") Unfinished
else
.panel-body
p(data-i18n="no_singleplayer") No Singleplayer games played yet.
.panel.panel-default
.panel-heading
h3.panel-title(data-i18n="no_multiplayer") Multiplayer Levels
if (!multiPlayerSessions)
.panel-body
p(data-i18n="common.loading") Loading...
else if (multiPlayerSessions.length)
table.table
tr
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="general.score") Score
each session in multiPlayerSessions
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('totalScore')
td= session.get('totalScore') * 100
else
td(data-i18n="user.status_unfinished") Unfinished
else
.panel-body
p(data-i18n="user.no_multiplayer") No Multiplayer games played yet.
.panel.panel-default
.panel-heading
h3.panel-title(data-i18n="user.achievements") Achievements
if ! earnedAchievements
.panel-body
p(data-i18n="common.loading") Loading...
else if ! earnedAchievements.length
.panel-body
p(data-i18n="user.no_achievements") No achievements earned so far.
else
table.table
tr
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.amount_achieved") Amount
each achievement in earnedAchievements.models
tr
td= achievement.get('achievementName')
td= moment().format("MMMM Do YYYY", achievement.get('changed'))
if achievement.get('achievedAmount')
td= achievement.get('achievedAmount')
else
td

View file

@ -1,4 +1,4 @@
CocoView = require 'views/kinds/CocoView'
RootView = require 'views/kinds/RootView'
ModalView = require 'views/kinds/ModalView'
template = require 'templates/demo'
requireUtils = require 'lib/requireUtils'
@ -24,8 +24,8 @@ DEMO_URL_PREFIX = '/demo/'
###
module.exports = DemoView = class DemoView extends CocoView
id: 'demo-view'
module.exports = DemoView = class DemoView extends RootView
id: "demo-view"
template: template
# INITIALIZE

View file

@ -0,0 +1,38 @@
View = require 'views/kinds/RootView'
template = require 'templates/account/account_home'
{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
id: 'account-home'
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,91 @@
CocoView = require 'views/kinds/CocoView'
template = require 'templates/achievements/achievement-popup'
User = require '../../models/User'
Achievement = require '../../models/Achievement'
module.exports = class AchievementPopup extends CocoView
className: 'achievement-popup'
template: template
constructor: (options) ->
@achievement = options.achievement
@earnedAchievement = options.earnedAchievement
@container = options.container or @getContainer()
@popup = options.container
@popup ?= true
@className += ' popup' if @popup
super options
console.debug 'Created an AchievementPopup', @$el
@render()
calculateData: ->
currentLevel = me.level()
nextLevel = currentLevel + 1
currentLevelExp = User.expForLevel(currentLevel)
nextLevelXP = User.expForLevel(nextLevel)
totalExpNeeded = nextLevelXP - currentLevelExp
expFunction = @achievement.getExpFunction()
currentXP = me.get 'points'
if @achievement.isRepeatable()
achievedXP = expFunction(@earnedAchievement.get('previouslyAchievedAmount')) * @achievement.get('worth') if @achievement.isRepeatable()
else
achievedXP = @achievement.get 'worth'
previousXP = currentXP - achievedXP
leveledUp = currentXP - achievedXP < currentLevelExp
#console.debug 'Leveled up' if leveledUp
alreadyAchievedPercentage = 100 * (previousXP - currentLevelExp) / totalExpNeeded
alreadyAchievedPercentage = 0 if alreadyAchievedPercentage < 0 # In case of level up
newlyAchievedPercentage = if leveledUp then 100 * (currentXP - currentLevelExp) / totalExpNeeded else 100 * achievedXP / totalExpNeeded
#console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelXP} xp)."
#console.debug "Need a total of #{nextLevelXP - currentLevelExp}, already had #{previousXP} and just now earned #{achievedXP} totalling on #{currentXP}"
data =
title: @achievement.i18nName()
imgURL: @achievement.getImageURL()
description: @achievement.i18nDescription()
level: currentLevel
currentXP: currentXP
newXP: achievedXP
leftXP: nextLevelXP - currentXP
oldXPWidth: alreadyAchievedPercentage
newXPWidth: newlyAchievedPercentage
leftXPWidth: 100 - newlyAchievedPercentage - alreadyAchievedPercentage
getRenderData: ->
c = super()
_.extend c, @calculateData()
c.style = @achievement.getStyle()
c.popup = true
c.$ = $ # Allows the jade template to do i18n
c
render: ->
console.debug 'render achievement popup'
super()
@container.prepend @$el
if @popup
@$el.animate
left: 0
@$el.on 'click', (e) =>
@$el.animate
left: 600
, =>
@$el.remove()
@destroy()
getContainer: ->
unless @container
@container = $('.achievement-popup-container')
unless @container.length
$('body').append('<div class="achievement-popup-container"></div>')
@container = $('.achievement-popup-container')
@container
afterRender: ->
super()
_.delay @initializeTooltips, 1000 # TODO this could be smoother
initializeTooltips: ->
$('.progress-bar').tooltip()

View file

@ -1,7 +1,10 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/editor/achievement/edit'
Achievement = require 'models/Achievement'
AchievementPopup = require 'views/achievements/AchievementPopup'
ConfirmModal = require 'views/modal/ConfirmModal'
errors = require 'lib/errors'
app = require 'application'
module.exports = class AchievementEditView extends RootView
id: 'editor-achievement-edit-view'
@ -11,6 +14,7 @@ module.exports = class AchievementEditView extends RootView
events:
'click #save-button': 'saveAchievement'
'click #recalculate-button': 'confirmRecalculation'
'click #delete-button': 'confirmDeletion'
subscriptions:
'save-new': 'saveAchievement'
@ -20,13 +24,10 @@ module.exports = class AchievementEditView extends RootView
@achievement = new Achievement(_id: @achievementID)
@achievement.saveBackups = true
@listenToOnce(@achievement, 'error',
() =>
@hideLoading()
$(@$el).find('.main-content-area').children('*').not('#error-view').remove()
@insertSubView(new ErrorView())
)
@achievement.once 'error', (achievement, jqxhr) =>
@hideLoading()
$(@$el).find('.main-content-area').children('*').not('.breadcrumb').remove()
errors.backboneFailure arguments...
@achievement.fetch()
@listenToOnce(@achievement, 'sync', @buildTreema)
@ -49,17 +50,31 @@ module.exports = class AchievementEditView extends RootView
@treema.build()
pushChangesToPreview: =>
'TODO' # TODO might want some intrinsic preview thing
getRenderData: (context={}) ->
context = super(context)
context.achievement = @achievement
context.authorized = me.isAdmin()
context
afterRender: ->
super(arguments...)
@pushChangesToPreview()
pushChangesToPreview: =>
$('#achievement-view').empty()
if @treema?
for key, value of @treema.data
@achievement.set key, value
earned =
earnedPoints: @achievement.get 'worth'
popup = new AchievementPopup achievement: @achievement, earnedAchievement:earned, popup: false, container: $('#achievement-view')
openSaveModal: ->
'Maybe later' # TODO
'Maybe later' # TODO patch patch patch
saveAchievement: (e) ->
@treema.endExistingEdits()
@ -75,20 +90,31 @@ module.exports = class AchievementEditView extends RootView
url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}"
document.location.href = url
confirmRecalculation: (e) ->
confirmRecalculation: ->
renderData =
'confirmTitle': 'Are you really sure?'
'confirmBody': 'This will trigger recalculation of the achievement for all users. Are you really sure you want to go down this path?'
'confirmDecline': 'Not really'
'confirmConfirm': 'Definitely'
confirmModal = new ConfirmModal(renderData)
confirmModal.onConfirm @recalculateAchievement
confirmModal = new ConfirmModal renderData
confirmModal.on 'confirm', @recalculateAchievement
@openModalView confirmModal
confirmDeletion: ->
renderData =
'confirmTitle': 'Are you really sure?'
'confirmBody': 'This will completely delete the achievement, 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', @deleteAchievement
@openModalView confirmModal
recalculateAchievement: =>
$.ajax
data: JSON.stringify(achievements: [@achievement.get('slug') or @achievement.get('_id')])
data: JSON.stringify(earnedAchievements: [@achievement.get('slug') or @achievement.get('_id')])
success: (data, status, jqXHR) ->
noty
timeout: 5000
@ -105,3 +131,24 @@ module.exports = class AchievementEditView extends RootView
url: '/admin/earned.achievement/recalculate'
type: 'POST'
contentType: 'application/json'
deleteAchievement: =>
console.debug 'deleting'
$.ajax
type: 'DELETE'
success: ->
noty
timeout: 5000
text: 'Aaaand it\'s gone.'
type: 'success'
layout: 'topCenter'
_.delay ->
app.router.navigate '/editor/achievement', trigger: true
, 500
error: (jqXHR, status, error) ->
console.error jqXHR
timeout: 5000
text: "Deleting achievement failed with error code #{jqXHR.status}"
type: 'error'
layout: 'topCenter'
url: "/db/achievement/#{@achievement.id}"

View file

@ -15,6 +15,7 @@ SaveLevelModal = require './modals/SaveLevelModal'
LevelForkView = require './modals/ForkLevelModal'
SaveVersionModal = require 'views/modal/SaveVersionModal'
PatchesView = require 'views/editor/PatchesView'
RelatedAchievementsView = require 'views/editor/level/RelatedAchievementsView'
VersionHistoryView = require './modals/LevelVersionsModal'
ComponentDocsView = require 'views/docs/ComponentDocumentationView'
@ -75,7 +76,8 @@ module.exports = class LevelEditView extends RootView
@insertSubView new ScriptsTabView world: @world, supermodel: @supermodel, files: @files
@insertSubView new ComponentsTabView supermodel: @supermodel
@insertSubView new SystemsTabView supermodel: @supermodel
@insertSubView new ComponentDocsView()
@insertSubView new RelatedAchievementsView supermodel: @supermodel, level: @level
@insertSubView new ComponentDocsView supermodel: @supermodel
Backbone.Mediator.publish 'level-loaded', level: @level
@showReadOnly() if me.get('anonymous')

View file

@ -0,0 +1,41 @@
CocoView = require 'views/kinds/CocoView'
template = require 'templates/editor/level/related-achievements'
RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection'
Achievement = require 'models/Achievement'
NewAchievementModal = require './modals/NewAchievementModal'
app = require 'application'
module.exports = class RelatedAchievementsView extends CocoView
id: 'related-achievements-view'
template: template
className: 'tab-pane'
events:
'click #new-achievement-button': 'makeNewAchievement'
constructor: (options) ->
super options
@level = options.level
@relatedID = @level.id
@achievements = new RelatedAchievementsCollection @relatedID
console.debug @achievements
@supermodel.loadCollection @achievements, 'achievements'
onLoaded: ->
console.debug 'related achievements loaded'
@achievements.loading = false
super()
getRenderData: ->
c = super()
c.achievements = @achievements
c.relatedID = @relatedID
c
onNewAchievementSaved: (achievement) ->
app.router.navigate('/editor/achievement/' + (achievement.get('slug') or achievement.id), {trigger: true})
makeNewAchievement: ->
modal = new NewAchievementModal model: Achievement, modelLabel: 'Achievement', level: @level
modal.once 'model-created', @onNewAchievementSaved
@openModalView modal

View file

@ -0,0 +1,54 @@
NewModelModal = require 'views/modal/NewModelModal'
template = require 'templates/editor/level/modal/new-achievement'
forms = require 'lib/forms'
Achievement = require 'models/Achievement'
module.exports = class NewAchievementModal extends NewModelModal
id: 'new-achievement-modal'
template: template
plain: false
constructor: (options) ->
super options
@level = options.level
getRenderData: ->
c = super()
c.level = @level
console.debug 'level', c.level
c
createQuery: ->
checked = @$el.find('[name=queryOptions]:checked')
checkedValues = ($(check).val() for check in checked)
subQueries = []
for id in checkedValues
switch id
when 'misc-level-completion'
subQueries.push state: complete: true
else # It's a goal
q = state: goalStates: {}
q.state.goalStates[id] = {}
q.state.goalStates[id].status = 'success'
subQueries.push q
unless subQueries.length
query = {}
else if subQueries.length is 1
query = subQueries[0]
else
query = $or: subQueries
query
makeNewModel: ->
achievement = new Achievement
name = @$el.find('#name').val()
description = @$el.find('#description').val()
query = @createQuery()
achievement.set 'name', name
achievement.set 'description', description
achievement.set 'query', query
achievement.set 'collection', 'level.sessions'
achievement.set 'userField', 'creator'
achievement

View file

@ -6,8 +6,9 @@ CocoView = require './CocoView'
{logoutUser, me} = require('lib/auth')
locale = require 'locale/locale'
Achievement = require '../../models/Achievement'
User = require '../../models/User'
AchievementPopup = require 'views/achievements/AchievementPopup'
utils = require 'lib/utils'
# TODO remove
filterKeyboardEvents = (allowedEvents, func) ->
@ -32,61 +33,13 @@ module.exports = class RootView extends CocoView
'achievements:new': 'handleNewAchievements'
showNewAchievement: (achievement, earnedAchievement) ->
currentLevel = me.level()
nextLevel = currentLevel + 1
currentLevelExp = User.expForLevel(currentLevel)
nextLevelExp = User.expForLevel(nextLevel)
totalExpNeeded = nextLevelExp - currentLevelExp
expFunction = achievement.getExpFunction()
currentExp = me.get('points')
previousExp = currentExp - achievement.get('worth')
previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable()
achievedExp = currentExp - previousExp
leveledUp = currentExp - achievedExp < currentLevelExp
alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / totalExpNeeded
newlyAchievedPercentage = if leveledUp then 100 * (currentExp - currentLevelExp) / totalExpNeeded else 100 * achievedExp / totalExpNeeded
console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelExp} xp)."
console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{previousExp} and just now earned #{achievedExp} totalling on #{currentExp}"
alreadyAchievedBar = $("<div class='progress-bar progress-bar-warning' style='width:#{alreadyAchievedPercentage}%'></div>")
newlyAchievedBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-success' style='width:#{newlyAchievedPercentage}%'></div>")
emptyBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-white' style='width:#{100 - newlyAchievedPercentage - alreadyAchievedPercentage}%'></div>")
progressBar = $('<div class="progress" data-toggle="tooltip"></div>').append(alreadyAchievedBar).append(newlyAchievedBar).append(emptyBar)
message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null
alreadyAchievedBar.tooltip(title: "#{currentExp} XP in total")
newlyAchievedBar.tooltip(title: "#{achievedExp} XP earned")
emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}")
# TODO a default should be linked here
imageURL = '/file/' + achievement.get('icon')
data =
title: achievement.get('name')
image: $("<img src='#{imageURL}' />")
description: achievement.get('description')
progressBar: progressBar
earnedExp: "+ #{achievedExp} XP"
message: message
options =
autoHideDelay: 10000
globalPosition: 'bottom right'
showDuration: 400
style: 'achievement'
autoHide: true
clickToHide: true
$.notify( data, options )
popup = new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement
handleNewAchievements: (earnedAchievements) ->
_.each(earnedAchievements.models, (earnedAchievement) =>
_.each earnedAchievements.models, (earnedAchievement) =>
achievement = new Achievement(_id: earnedAchievement.get('achievement'))
console.log achievement
achievement.fetch(
achievement.fetch
success: (achievement) => @showNewAchievement(achievement, earnedAchievement)
)
)
logoutAccount: ->
logoutUser($('#login-email').val())

View file

@ -1,6 +1,6 @@
RootView = require 'views/kinds/RootView'
NewModelModal = require 'views/modal/NewModelModal'
template = require 'templates/kinds/search'
forms = require 'lib/forms'
app = require 'application'
class SearchCollection extends Backbone.Collection
@ -26,9 +26,7 @@ module.exports = class SearchView extends RootView
events:
'change input#search': 'runSearch'
'keydown input#search': 'runSearch'
'click button.new-model-submit': 'makeNewModel'
'submit form': 'makeNewModel'
'shown.bs.modal #new-model-modal': 'focusOnName'
'click #new-model-button': 'newModel'
'hidden.bs.modal #new-model-modal': 'onModalHidden'
constructor: (options) ->
@ -79,31 +77,11 @@ module.exports = class SearchView extends RootView
@collection.off()
@collection = null
makeNewModel: (e) ->
e.preventDefault()
name = @$el.find('#name').val()
model = new @model()
model.set('name', name)
if @model.schema.properties.permissions
model.set 'permissions', [{access: 'owner', target: me.id}]
res = model.save()
return unless res
modal = @$el.find('#new-model-modal')
forms.clearFormAlerts(modal)
@showLoading(modal.find('.modal-body'))
res.error =>
@hideLoading()
forms.applyErrorsToForm(modal, JSON.parse(res.responseText))
that = @
res.success ->
that.model = model
modal.modal('hide')
onModalHidden: ->
# Can only redirect after the modal hidden event has triggered
onNewModelSaved: (@model) ->
base = document.location.pathname[1..] + '/'
app.router.navigate(base + (@model.get('slug') or @model.id), {trigger: true})
focusOnName: ->
@$el.find('#name').focus()
newModel: (e) ->
modal = new NewModelModal model: @model, modelLabel: @modelLabel
modal.once 'success', @onNewModelSaved
@openModalView modal

View file

@ -0,0 +1,35 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/kinds/user'
User = require 'models/User'
module.exports = class UserView extends RootView
template: template
className: 'user-view'
viewName: null # Used for the breadcrumbs
constructor: (@userID, options) ->
super options
@listenTo @, 'userNotFound', @ifUserNotFound
@fetchUser @userID
fetchUser: (id) ->
if @isMe()
@user = me
@onLoaded()
@user = new User _id: id
@supermodel.loadModel @user, 'user'
getRenderData: ->
context = super()
context.viewName = @viewName
context.user = @user unless @user?.isAnonymous()
context
isMe: -> @userID is me.id
onLoaded: ->
super()
ifUserNotFound: ->
console.warn 'user not found'
@render()

View file

@ -8,8 +8,8 @@ module.exports = class ConfirmModal extends ModalView
closeOnConfirm: true
events:
'click #decline-button': 'doDecline'
'click #confirm-button': 'doConfirm'
'click #decline-button': 'onDecline'
'click #confirm-button': 'onConfirm'
constructor: (@renderData={}, options={}) ->
super(options)
@ -21,10 +21,6 @@ module.exports = class ConfirmModal extends ModalView
setRenderData: (@renderData) ->
onDecline: (@decline) ->
onDecline: -> @trigger 'decline'
onConfirm: (@confirm) ->
doConfirm: -> @confirm() if @confirm
doDecline: -> @decline() if @decline
onConfirm: -> @trigger 'confirm'

View file

@ -0,0 +1,54 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/modal/new_model'
forms = require 'lib/forms'
module.exports = class NewModelModal extends ModalView
id: 'new-model-modal'
template: template
plain: false
events:
'click button.new-model-submit': 'onModelSubmitted'
'submit form': 'onModelSubmitted'
constructor: (options) ->
super options
@model = options.model
@modelLabel = options.modelLabel
@properties = options.properties
$('#name').ready @focusOnName
getRenderData: ->
c = super()
c.modelLabel = @modelLabel
#c.newModelTitle = @newModelTitle
c
makeNewModel: ->
model = new @model
name = @$el.find('#name').val()
model.set('name', name)
if @model.schema.properties.permissions
model.set 'permissions', [{access: 'owner', target: me.id}]
model.set(key, prop) for key, prop of @properties if @properties?
model
onModelSubmitted: (e) ->
e.preventDefault()
model = @makeNewModel()
res = model.save()
return unless res
forms.clearFormAlerts @$el
@showLoading(@$el.find('.modal-body'))
res.error =>
@hideLoading()
forms.applyErrorsToForm(@$el, JSON.parse(res.responseText))
#Backbone.Mediator.publish 'model-save-fail', model
res.success =>
@$el.modal('hide')
@trigger 'model-created', model
#Backbone.Mediator.publish 'model-save-success', model
focusOnName: (e) ->
$('#name').focus() # TODO Why isn't this working anymore.. It does get called

View file

@ -0,0 +1,56 @@
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.achievements = @achievements
context.achievementsByCategory = {}
for achievement in @achievements.models
context.achievementsByCategory[achievement.get('category')] ?= []
context.achievementsByCategory[achievement.get('category')].push achievement
context

View file

@ -1,4 +1,4 @@
RootView = require 'views/kinds/RootView'
UserView = require 'views/kinds/UserView'
template = require 'templates/account/profile'
User = require 'models/User'
LevelSession = require 'models/LevelSession'
@ -26,7 +26,7 @@ adminContacts = [
{id: '52a57252a89409700d0000d9', name: 'Ignore'}
]
module.exports = class JobProfileView extends RootView
module.exports = class JobProfileView extends UserView
id: 'profile-view'
template: template
showBackground: false
@ -54,8 +54,7 @@ module.exports = class JobProfileView extends RootView
'change #admin-contact': 'onAdminContactChanged'
'click .session-link': 'onSessionLinkPressed'
constructor: (options, @userID) ->
@userID ?= me.id
constructor: (userID, options) ->
@onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000
@onRemarkChanged = _.debounce @onRemarkChanged, 1000
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
@ -64,32 +63,19 @@ module.exports = class JobProfileView extends RootView
window.contractCallback = =>
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
@render()
super options
if me.get('anonymous') is true
@render()
return
if User.isObjectID @userID
@finishInit()
else
$.ajax "/db/user/#{@userID}/nameToID", success: (@userID) =>
@finishInit() unless @destroyed
@render()
super options, userID
onUserLoaded: ->
@finishInit() unless @destroyed
super()
finishInit: ->
return unless @userID
@uploadFilePath = "db/user/#{@userID}"
@highlightedContainers = []
if @userID is me.id
@user = me
else if me.isAdmin() or 'employer' in me.get('permissions')
@user = User.getByID(@userID)
@user.fetch()
@listenTo @user, 'sync', =>
@render()
if me.isAdmin() or 'employer' in me.get('permissions')
$.post "/db/user/#{me.id}/track/view_candidate"
$.post "/db/user/#{@userID}/track/viewed_by_employer" unless me.isAdmin()
else
@user = User.getByID(@userID)
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@userID), 'candidate_sessions').model
if me.isAdmin()
# Mimicking how the VictoryModal fetches LevelFeedback
@ -248,7 +234,7 @@ module.exports = class JobProfileView extends RootView
jobProfile.name ?= (@user.get('firstName') + ' ' + @user.get('lastName')).trim() if @user?.get('firstName')
context.profile = jobProfile
context.user = @user
context.myProfile = @user?.id is context.me.id
context.myProfile = @isMe()
context.allowedToViewJobProfile = @user and (me.isAdmin() or 'employer' in me.get('permissions') or (context.myProfile && !me.get('anonymous')))
context.allowedToEditJobProfile = @user and (me.isAdmin() or (context.myProfile && !me.get('anonymous')))
context.profileApproved = @user?.get 'jobProfileApproved'
@ -289,7 +275,7 @@ module.exports = class JobProfileView extends RootView
_.delay ->
justSavedSection.removeClass 'just-saved', duration: 1500, easing: 'easeOutQuad'
, 500
if me.isAdmin()
if me.isAdmin() and @user
visibleSettings = ['history', 'tasks']
data = _.pick (@remark.attributes), (value, key) -> key in visibleSettings
data.history ?= []
@ -596,4 +582,4 @@ module.exports = class JobProfileView extends RootView
sessionID = $(e.target).closest('.session-link').data('session-id')
session = _.find @sessions.models, (session) -> session.id is sessionID
modal = new JobProfileCodeModal({session:session})
@openModalView modal
@openModalView modal

View file

@ -0,0 +1,54 @@
UserView = require 'views/kinds/UserView'
CocoCollection = require 'collections/CocoCollection'
LevelSession = require 'models/LevelSession'
template = require 'templates/user/user_home'
{me} = require 'lib/auth'
EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
class LevelSessionsCollection extends CocoCollection
model: LevelSession
constructor: (userID) ->
@url = "/db/user/#{userID}/level.sessions?project=state.complete,levelID,levelName,changed,team,submittedCodeLanguage,totalScore&order=-1"
super()
module.exports = class MainUserView extends UserView
id: 'user-home'
template: template
constructor: (userID, options) ->
super options
getRenderData: ->
context = super()
if @levelSessions and @levelSessions.loaded
singlePlayerSessions = []
multiPlayerSessions = []
languageCounts = {}
for levelSession in @levelSessions.models
if levelSession.isMultiplayer()
multiPlayerSessions.push levelSession
else
singlePlayerSessions.push levelSession
languageCounts[levelSession.get 'submittedCodeLanguage'] = (languageCounts[levelSession.get 'submittedCodeLanguage'] or 0) + 1
mostUsedCount = 0
favoriteLanguage = null
for language, count of languageCounts
if count > mostUsedCount
mostUsedCount = count
favoriteLanguage = language
context.singlePlayerSessions = singlePlayerSessions
context.multiPlayerSessions = multiPlayerSessions
context.favoriteLanguage = favoriteLanguage
if @earnedAchievements and @earnedAchievements.loaded
context.earnedAchievements = @earnedAchievements
context
onLoaded: ->
if @user.loaded and not (@earnedAchievements or @levelSessions)
@supermodel.resetProgress()
@levelSessions = new LevelSessionsCollection @user.getSlugOrID()
@earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID()
@supermodel.loadCollection @levelSessions, 'levelSessions'
@supermodel.loadCollection @earnedAchievements, 'earnedAchievements'
super()

View file

@ -42,6 +42,7 @@
"winston": "0.6.x",
"passport": "0.1.x",
"passport-local": "0.1.x",
"moment": "~2.5.0",
"mongodb": "1.2.x",
"mongoose": "3.8.x",
"mongoose-text-search": "~0.0.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,19 @@
database = require '../server/commons/database'
mongoose = require 'mongoose'
log = require 'winston'
async = require 'async'
### SET UP ###
do (setupLodash = this) ->
GLOBAL._ = require 'lodash'
_.str = require 'underscore.string'
_.mixin _.str.exports()
database.connect()
EarnedAchievementHandler = require '../server/achievements/earned_achievement_handler'
log.info 'Starting earned achievement recalculation...'
EarnedAchievementHandler.constructor.recalculate (err) ->
log.error err if err?
log.info 'Finished recalculating all earned achievements.'
process.exit()

View file

@ -0,0 +1,54 @@
database = require '../server/commons/database'
mongoose = require 'mongoose'
log = require 'winston'
async = require 'async'
### SET UP ###
do (setupLodash = this) ->
GLOBAL._ = require 'lodash'
_.str = require 'underscore.string'
_.mixin _.str.exports()
database.connect()
### USER STATS ###
UserHandler = require '../server/users/user_handler'
report = (func, name, done) ->
log.info 'Started ' + name + '...'
func name, (err) ->
log.warn err if err?
log.info 'Finished ' + name
done err if done?
whenAllFinished = ->
log.info 'All recalculations finished.'
process.exit()
async.parallel [
# Misc
(c) -> report UserHandler.recalculateStats, 'gamesCompleted', c
# Edits
(c) -> report UserHandler.recalculateStats, 'articleEdits', c
(c) -> report UserHandler.recalculateStats, 'levelEdits', c
(c) -> report UserHandler.recalculateStats, 'levelComponentEdits', c
(c) -> report UserHandler.recalculateStats, 'levelSystemEdits', c
(c) -> report UserHandler.recalculateStats, 'thangTypeEdits', c
# Patches
(c) -> report UserHandler.recalculateStats, 'patchesContributed', c
(c) -> report UserHandler.recalculateStats, 'patchesSubmitted', c
(c) -> report UserHandler.recalculateStats, 'totalTranslationPatches', c
(c) -> report UserHandler.recalculateStats, 'totalMiscPatches', c
(c) -> report UserHandler.recalculateStats, 'articleMiscPatches', c
(c) -> report UserHandler.recalculateStats, 'levelMiscPatches', c
(c) -> report UserHandler.recalculateStats, 'levelComponentMiscPatches', c
(c) -> report UserHandler.recalculateStats, 'levelSystemMiscPatches', c
(c) -> report UserHandler.recalculateStats, 'thangTypeMiscPatches', c
(c) -> report UserHandler.recalculateStats, 'articleTranslationPatches', c
(c) -> report UserHandler.recalculateStats, 'levelTranslationPatches', c
(c) -> report UserHandler.recalculateStats, 'levelComponentTranslationPatches', c
(c) -> report UserHandler.recalculateStats, 'levelSystemTranslationPatches', c
(c) -> report UserHandler.recalculateStats, 'thangTypeTranslationPatches', c
], whenAllFinished

View file

@ -0,0 +1,232 @@
database = require '../server/commons/database'
mongoose = require 'mongoose'
log = require 'winston'
async = require 'async'
### SET UP ###
do (setupLodash = this) ->
GLOBAL._ = require 'lodash'
_.str = require 'underscore.string'
_.mixin _.str.exports()
database.connect()
## Util
## Types
contributor = (obj) ->
_.extend obj, # This way we get the name etc on top
collection: 'users'
userField: '_id'
category: 'contributor'
### UNLOCKABLES ###
# Generally ordered according to user.stats schema
unlockableAchievements =
signup:
name: 'Signed Up'
description: 'Signed up to the most awesome coding game around.'
query: 'anonymous': false
worth: 10
collection: 'users'
userField: '_id'
category: 'miscellaneous'
difficulty: 1
recalculable: true
completedFirstLevel:
name: 'Completed 1 Level'
description: 'Completed your very first level.'
query: 'stats.gamesCompleted': $gte: 1
worth: 20
collection: 'users'
userField: '_id'
category: 'levels'
difficulty: 1
recalculable: true
completedFiveLevels:
name: 'Completed 5 Levels'
description: 'Completed 5 Levels.'
query: 'stats.gamesCompleted': $gte: 5
worth: 50
collection: 'users'
userField: '_id'
category: 'levels'
difficulty: 2
recalculable: true
completedTwentyLevels:
name: 'Completed 20 Levels'
description: 'Completed 20 Levels.'
query: 'stats.gamesCompleted': $gte: 20
worth: 500
collection: 'users'
userField: '_id'
category: 'levels'
difficulty: 3
recalculable: true
editedOneArticle: contributor
name: 'Edited an Article'
description: 'Edited your first Article.'
query: 'stats.articleEdits': $gte: 1
worth: 50
difficulty: 1
editedOneLevel: contributor
name: 'Edited a Level'
description: 'Edited your first Level.'
query: 'stats.levelEdits': $gte: 1
worth: 50
difficulty: 1
recalculable: true
editedOneLevelSystem: contributor
name: 'Edited a Level System'
description: 'Edited your first Level System.'
query: 'stats.levelSystemEdits': $gte: 1
worth: 50
difficulty: 1
recalculable: true
editedOneLevelComponent: contributor
name: 'Edited a Level Component'
description: 'Edited your first Level Component.'
query: 'stats.levelComponentEdits': $gte: 1
worth: 50
difficulty: 1
recalculable: true
editedOneThangType: contributor
name: 'Edited a Thang Type'
description: 'Edited your first Thang Type.'
query: 'stats.thangTypeEdits': $gte: 1
worth: 50
difficulty: 1
recalculable: true
submittedOnePatch: contributor
name: 'Submitted a Patch'
description: 'Submitted your very first patch.'
query: 'stats.patchesSubmitted': $gte: 1
worth: 50
difficulty: 1
recalculable: true
contributedOnePatch: contributor
name: 'Contributed a Patch'
description: 'Got your very first accepted Patch.'
query: 'stats.patchesContributed': $gte: 1
worth: 50
difficulty: 1
recalculable: true
acceptedOnePatch: contributor
name: 'Accepted a Patch'
description: 'Accepted your very first patch.'
query: 'stats.patchesAccepted': $gte: 1
worth: 50
difficulty: 1
recalculable: false
oneTranslationPatch: contributor
name: 'First Translation'
description: 'Did your very first translation.'
query: 'stats.totalTranslationPatches': $gte: 1
worth: 50
difficulty: 1
recalculable: true
oneMiscPatch: contributor
name: 'First Miscellaneous Patch'
description: 'Did your first miscellaneous patch.'
query: 'stats.totalMiscPatches': $gte: 1
worth: 50
difficulty: 1
recalculable: true
oneArticleTranslationPatch: contributor
name: 'First Article Translation'
description: 'Did your very first Article translation.'
query: 'stats.articleTranslationPatches': $gte: 1
worth: 50
difficulty: 1
recalculable: true
oneArticleMiscPatch: contributor
name: 'First Misc Article Patch'
description: 'Did your first miscellaneous Article patch.'
query: 'stats.totalMiscPatches': $gte: 1
worth: 50
difficulty: 1
recalculable: true
oneLevelTranslationPatch: contributor
name: 'First Level Translation'
description: 'Did your very first Level translation.'
query: 'stats.levelTranslationPatches': $gte: 1
worth: 50
difficulty: 1
recalculable: true
oneLevelMiscPatch: contributor
name: 'First Misc Level Patch'
description: 'Did your first misc Level patch.'
query: 'stats.levelMiscPatches': $gte: 1
worth: 50
difficulty: 1
recalculable: true
### REPEATABLES ###
repeatableAchievements =
simulatedBy:
name: 'Simulated ladder game'
description: 'Simulated a ladder game.'
query: 'simulatedBy': $gte: 1
worth: 1
collection: 'users'
userField: '_id'
category: 'miscellaneous'
difficulty: 1
proportionalTo: 'simulatedBy'
function:
kind: 'logarithmic'
parameters: # TODO tweak
a: 5
b: 1
c: 0
Achievement = require '../server/achievements/Achievement'
EarnedAchievement = require '../server/achievements/EarnedAchievement'
Achievement.find {}, (err, achievements) ->
achievementIDs = (achievement.get('_id') + '' for achievement in achievements)
EarnedAchievement.remove {achievement: $in: achievementIDs}, (err, count) ->
return log.error err if err?
log.info "Removed #{count} earned achievements that were related"
Achievement.remove {}, (err) ->
log.error err if err?
log.info 'Removed all achievements.'
log.info "Got #{Object.keys(unlockableAchievements).length} unlockable achievements"
log.info "and #{Object.keys(repeatableAchievements).length} repeatable achievements"
achievements = _.extend unlockableAchievements, repeatableAchievements
async.each Object.keys(achievements), (key, callback) ->
achievement = achievements[key]
log.info "Setting up '#{achievement.name}'..."
achievementM = new Achievement achievement
# What the actual * Mongoose? It automatically converts 'stats.edits' to a nested object
achievementM.set 'query', achievement.query
log.debug JSON.stringify achievementM.get 'query'
achievementM.save (err) ->
log.error err if err?
callback()
, (err) ->
log.error err if err?
log.info 'Finished setting up achievements.'
process.exit()

View file

@ -1,8 +1,9 @@
mongoose = require 'mongoose'
jsonschema = require '../../app/schemas/models/achievement'
log = require 'winston'
util = require '../../app/lib/utils'
plugins = require '../plugins/plugins'
utils = require '../../app/lib/utils'
plugins = require('../plugins/plugins')
AchievablePlugin = require '../plugins/achievements'
# `pre` and `post` are not called for update operations executed directly on the database,
# including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order
@ -23,23 +24,48 @@ AchievementSchema.methods.objectifyQuery = ->
AchievementSchema.methods.stringifyQuery = ->
@set('query', JSON.stringify(@get('query'))) if typeof @get('query') != 'string'
getExpFunction: ->
kind = @get('function')?.kind or jsonschema.function.default.kind
parameters = @get('function')?.parameters or jsonschema.function.default.parameters
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
AchievementSchema.methods.getExpFunction = ->
kind = @get('function')?.kind or jsonschema.properties.function.default.kind
parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
AchievementSchema.post('init', (doc) -> doc.objectifyQuery())
AchievementSchema.methods.isRecalculable = -> @get('recalculable') is true
AchievementSchema.pre('save', (next) ->
AchievementSchema.statics.jsonschema = jsonschema
AchievementSchema.statics.earnedAchievements = {}
# Reloads all achievements into memory.
# TODO might want to tweak this to only load new achievements
AchievementSchema.statics.loadAchievements = (done) ->
AchievementSchema.statics.resetAchievements()
Achievement = require('../achievements/Achievement')
query = Achievement.find({})
query.exec (err, docs) ->
_.each docs, (achievement) ->
category = achievement.get 'collection'
AchievementSchema.statics.earnedAchievements[category] = [] unless category of AchievementSchema.statics.earnedAchievements
AchievementSchema.statics.earnedAchievements[category].push achievement
done?(AchievementSchema.statics.earnedAchievements)
AchievementSchema.statics.getLoadedAchievements = ->
AchievementSchema.statics.earnedAchievements
AchievementSchema.statics.resetAchievements = ->
delete AchievementSchema.statics.earnedAchievements[category] for category of AchievementSchema.statics.earnedAchievements
# Queries are stored as JSON strings, objectify them upon loading
AchievementSchema.post 'init', (doc) -> doc.objectifyQuery()
AchievementSchema.pre 'save', (next) ->
@stringifyQuery()
next()
)
# Reload achievements upon save
AchievementSchema.post 'save', -> @constructor.loadAchievements()
AchievementSchema.plugin(plugins.NamedPlugin)
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema)
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema, 'achievements')
# Reload achievements upon save
AchievablePlugin = require '../plugins/achievements'
AchievementSchema.post 'save', (doc) -> AchievablePlugin.loadAchievements()
AchievementSchema.statics.loadAchievements()

View file

@ -5,10 +5,32 @@ class AchievementHandler extends Handler
modelClass: Achievement
# Used to determine which properties requests may edit
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function']
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable']
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
jsonSchema = require '../../app/schemas/models/achievement.coffee'
hasAccess: (req) ->
req.method is 'GET' or req.user?.isAdmin()
get: (req, res) ->
# /db/achievement?related=<ID>
if req.query.related
return @sendUnauthorizedError(res) if not @hasAccess(req)
Achievement.find {related: req.query.related}, (err, docs) =>
return @sendDatabaseError(res, err) if err
docs = (@formatEntity(req, doc) for doc in docs)
@sendSuccess res, docs
else
super req, res
delete: (req, res, slugOrID) ->
return @sendUnauthorizedError res unless req.user?.isAdmin()
@getDocumentForIdOrSlug slugOrID, (err, document) => # Check first
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 AchievementHandler()

Some files were not shown because too many files have changed in this diff Show more