mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 00:40:56 -05:00
Added achievement deleting and automatic achievement filling
This commit is contained in:
parent
3bfd341363
commit
47f00f9b5e
15 changed files with 182 additions and 50 deletions
|
@ -195,6 +195,7 @@ module.exports = class LevelBus extends Bus
|
|||
@saveSession()
|
||||
|
||||
onNewGoalStates: ({goalStates})->
|
||||
console.debug arguments
|
||||
state = @session.get 'state'
|
||||
unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change
|
||||
state.goalStates = goalStates
|
||||
|
|
|
@ -11,6 +11,7 @@ block content
|
|||
| #{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
|
||||
|
|
23
app/templates/editor/level/modal/new-achievement.jade
Normal file
23
app/templates/editor/level/modal/new-achievement.jade
Normal 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") Description
|
||||
input#description.form-control(name="description", type="text")
|
||||
h4 Miscellaneous achievement keys
|
||||
.radio
|
||||
label
|
||||
input(type="checkbox", name="queryOptions" id="misc-level-completion" value="misc-level-completion")
|
||||
span.spl Level Completion
|
||||
- var goals = level.get('goals');
|
||||
if goals && goals.length
|
||||
h4 Base achievement on goals?
|
||||
each goal in goals
|
||||
.radio
|
||||
label
|
||||
input(type="checkbox", name="queryOptions" id="#{goal.id}" value="#{goal.id}")
|
||||
span.spl= goal.name
|
|
@ -3,6 +3,7 @@ template = require 'templates/editor/achievement/edit'
|
|||
Achievement = require 'models/Achievement'
|
||||
ConfirmModal = require 'views/modal/ConfirmModal'
|
||||
errors = require 'lib/errors'
|
||||
app = require 'application'
|
||||
|
||||
module.exports = class AchievementEditView extends RootView
|
||||
id: 'editor-achievement-edit-view'
|
||||
|
@ -12,6 +13,7 @@ module.exports = class AchievementEditView extends RootView
|
|||
events:
|
||||
'click #save-button': 'saveAchievement'
|
||||
'click #recalculate-button': 'confirmRecalculation'
|
||||
'click #delete-button': 'confirmDeletion'
|
||||
|
||||
subscriptions:
|
||||
'save-new': 'saveAchievement'
|
||||
|
@ -96,15 +98,26 @@ 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: =>
|
||||
|
@ -126,3 +139,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}"
|
||||
|
|
|
@ -15,7 +15,7 @@ SaveLevelModal = require './modals/SaveLevelModal'
|
|||
LevelForkView = require './modals/ForkLevelModal'
|
||||
SaveVersionModal = require 'views/modal/SaveVersionModal'
|
||||
PatchesView = require 'views/editor/PatchesView'
|
||||
RelatedAchievementsView = require 'views/editor/RelatedAchievementsView'
|
||||
RelatedAchievementsView = require 'views/editor/level/RelatedAchievementsView'
|
||||
VersionHistoryView = require './modals/LevelVersionsModal'
|
||||
ComponentDocsView = require 'views/docs/ComponentDocumentationView'
|
||||
|
||||
|
@ -75,7 +75,7 @@ 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 RelatedAchievementsView supermodel: @supermodel, relatedID: @level.id
|
||||
@insertSubView new RelatedAchievementsView supermodel: @supermodel, level: @level
|
||||
@insertSubView new ComponentDocsView supermodel: @supermodel
|
||||
|
||||
Backbone.Mediator.publish 'level-loaded', level: @level
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
CocoView = require 'views/kinds/CocoView'
|
||||
template = require 'templates/editor/related-achievements'
|
||||
template = require 'templates/editor/level/related-achievements'
|
||||
RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection'
|
||||
Achievement = require 'models/Achievement'
|
||||
NewModelModal = require 'views/modal/NewModelModal'
|
||||
NewAchievementModal = require './modals/NewAchievementModal'
|
||||
app = require 'application'
|
||||
|
||||
module.exports = class RelatedAchievementsView extends CocoView
|
||||
|
@ -15,7 +15,8 @@ module.exports = class RelatedAchievementsView extends CocoView
|
|||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@relatedID = options.relatedID
|
||||
@level = options.level
|
||||
@relatedID = @level.id
|
||||
@achievements = new RelatedAchievementsCollection @relatedID
|
||||
console.debug @achievements
|
||||
@supermodel.loadCollection @achievements, 'achievements'
|
||||
|
@ -31,14 +32,10 @@ module.exports = class RelatedAchievementsView extends CocoView
|
|||
c.relatedID = @relatedID
|
||||
c
|
||||
|
||||
render: ->
|
||||
console.debug 'rendering achievements'
|
||||
super()
|
||||
|
||||
onNewAchievementSaved: (achievement) ->
|
||||
app.router.navigate('/editor/achievement/' + (achievement.get('slug') or achievement.id), {trigger: true})
|
||||
|
||||
makeNewAchievement: ->
|
||||
modal = new NewModelModal model: Achievement, modelLabel: 'Achievement', properties: related: @relatedID
|
||||
modal.once 'success', @onNewAchievementSaved
|
||||
modal = new NewAchievementModal model: Achievement, modelLabel: 'Achievement', level: @level
|
||||
modal.once 'model-created', @onNewAchievementSaved
|
||||
@openModalView modal
|
54
app/views/editor/level/modals/NewAchievementModal.coffee
Normal file
54
app/views/editor/level/modals/NewAchievementModal.coffee
Normal 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
|
|
@ -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'
|
||||
|
|
|
@ -8,15 +8,15 @@ module.exports = class NewModelModal extends ModalView
|
|||
plain: false
|
||||
|
||||
events:
|
||||
'click button.new-model-submit': 'makeNewModel'
|
||||
'submit form': 'makeNewModel'
|
||||
'shown.bs.modal #new-model-modal': 'focusOnName'
|
||||
'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()
|
||||
|
@ -24,14 +24,19 @@ module.exports = class NewModelModal extends ModalView
|
|||
#c.newModelTitle = @newModelTitle
|
||||
c
|
||||
|
||||
makeNewModel: (e) ->
|
||||
e.preventDefault()
|
||||
name = @$el.find('#name').val()
|
||||
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) ->
|
||||
console.debug 'on model submitted'
|
||||
e.preventDefault()
|
||||
model = @makeNewModel()
|
||||
res = model.save()
|
||||
return unless res
|
||||
|
||||
|
@ -43,6 +48,8 @@ module.exports = class NewModelModal extends ModalView
|
|||
#Backbone.Mediator.publish 'model-save-fail', model
|
||||
res.success =>
|
||||
@$el.modal('hide')
|
||||
@trigger 'success', model
|
||||
@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
|
||||
|
|
|
@ -5,9 +5,11 @@ class AchievementHandler extends Handler
|
|||
modelClass: Achievement
|
||||
|
||||
# Used to determine which properties requests may edit
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category']
|
||||
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()
|
||||
|
||||
|
@ -22,4 +24,13 @@ class AchievementHandler extends Handler
|
|||
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()
|
||||
|
|
|
@ -71,21 +71,17 @@ class EarnedAchievementHandler extends Handler
|
|||
|
||||
isRepeatable = achievement.get('proportionalTo')?
|
||||
model = mongoose.modelNameByCollection(achievement.get('collection'))
|
||||
if not model?
|
||||
log.error "Model with collection '#{achievement.get 'collection'}' doesn't exist."
|
||||
return doneWithAchievement()
|
||||
return doneWithAchievement new Error "Model with collection '#{achievement.get 'collection'}' doesn't exist." unless model?
|
||||
|
||||
finalQuery = _.clone achievement.get 'query'
|
||||
finalQuery.$or = [{}, {}] # Allow both ObjectIDs or hex string IDs
|
||||
finalQuery.$or[0][achievement.userField] = userID
|
||||
finalQuery.$or[1][achievement.userField] = mongoose.Types.ObjectId userID
|
||||
|
||||
log.debug JSON.stringify finalQuery
|
||||
|
||||
model.findOne finalQuery, (err, something) ->
|
||||
return doneWithAchievement() if _.isEmpty something
|
||||
|
||||
log.debug "Matched an achievement: #{achievement.get 'name'} for #{user.get 'name'}"
|
||||
#log.debug "Matched an achievement: #{achievement.get 'name'} for #{user.get 'name'}"
|
||||
|
||||
earned =
|
||||
user: userID
|
||||
|
@ -107,7 +103,7 @@ class EarnedAchievementHandler extends Handler
|
|||
|
||||
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||
doneWithAchievement err
|
||||
), saveUserPoints = ->
|
||||
), -> # Wrap up a user, save points
|
||||
# Since some achievements cannot be recalculated it's important to deduct the old amount of exp
|
||||
# and add the new amount, instead of just setting to the new amount
|
||||
return doneWithUser() unless newTotalPoints
|
||||
|
|
|
@ -57,7 +57,7 @@ module.exports = class Handler
|
|||
sendUnauthorizedError: (res) -> errors.forbidden(res) #TODO: rename sendUnauthorizedError to sendForbiddenError
|
||||
sendForbiddenError: (res) -> errors.forbidden(res)
|
||||
sendNotFoundError: (res, message) -> errors.notFound(res, message)
|
||||
sendMethodNotAllowed: (res) -> errors.badMethod(res)
|
||||
sendMethodNotAllowed: (res, message) -> errors.badMethod(res, @allowedMethods, message)
|
||||
sendBadInputError: (res, message) -> errors.badInput(res, message)
|
||||
sendDatabaseError: (res, err) ->
|
||||
return @sendError(res, err.code, err.response) if err.response and err.code
|
||||
|
@ -79,8 +79,8 @@ module.exports = class Handler
|
|||
res.send 202, message
|
||||
res.end()
|
||||
|
||||
sendNoContent: (res, message) ->
|
||||
res.send 204, message
|
||||
sendNoContent: (res) ->
|
||||
res.send 204
|
||||
res.end()
|
||||
|
||||
# generic handlers
|
||||
|
@ -453,9 +453,9 @@ module.exports = class Handler
|
|||
res.send dict
|
||||
res.end()
|
||||
|
||||
delete: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, 'DELETE not allowed.'
|
||||
delete: (req, res) -> @sendMethodNotAllowed res, 'DELETE not allowed.'
|
||||
|
||||
head: (req, res) -> @sendMethodNotAllowed res, @allowedMethods, 'HEAD not allowed.'
|
||||
head: (req, res) -> @sendMethodNotAllowed res, 'HEAD not allowed.'
|
||||
|
||||
# This is not a Mongoose user
|
||||
projectionForUser: (req, model, ownerID) ->
|
||||
|
|
|
@ -38,7 +38,7 @@ module.exports.setup = (app) ->
|
|||
return handler.getByRelationship(req, res, parts[1..]...) if parts.length > 2
|
||||
return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]?
|
||||
return handler.patch(req, res, parts[1]) if req.route.method is 'patch' and parts[1]?
|
||||
handler[req.route.method](req, res)
|
||||
handler[req.route.method](req, res, parts[1..]...)
|
||||
catch error
|
||||
log.error("Error trying db method #{req.route.method} route #{parts} from #{name}: #{error}")
|
||||
log.error(error)
|
||||
|
|
|
@ -7,6 +7,10 @@ unlockable =
|
|||
collection: 'level.sessions'
|
||||
query: "{\"level.original\":\"dungeon-arena\"}"
|
||||
userField: 'creator'
|
||||
recalculable: true
|
||||
|
||||
unlockable2 = _.clone unlockable
|
||||
unlockable2.name = 'This one is obsolete'
|
||||
|
||||
repeatable =
|
||||
name: 'Simulated'
|
||||
|
@ -16,6 +20,7 @@ repeatable =
|
|||
query: "{\"simulatedBy\":{\"$gt\":0}}"
|
||||
userField: '_id'
|
||||
proportionalTo: 'simulatedBy'
|
||||
recalculable: true
|
||||
|
||||
diminishing =
|
||||
name: 'Simulated2'
|
||||
|
@ -27,11 +32,12 @@ diminishing =
|
|||
function:
|
||||
kind: 'logarithmic'
|
||||
parameters: {a: 1, b: .5, c: .5, d: 1}
|
||||
recalculable: true
|
||||
|
||||
url = getURL('/db/achievement')
|
||||
|
||||
describe 'Achievement', ->
|
||||
allowHeader = 'GET, POST, PUT, PATCH'
|
||||
allowHeader = 'GET, POST, PUT, PATCH, DELETE'
|
||||
|
||||
it 'preparing test: deleting all Achievements first', (done) ->
|
||||
clearModels [Achievement, EarnedAchievement, LevelSession, User], (err) ->
|
||||
|
@ -92,12 +98,18 @@ describe 'Achievement', ->
|
|||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
|
||||
it 'can\'t be requested with HTTP DEL method', (done) ->
|
||||
loginJoe ->
|
||||
request.del {uri: url + '/' + unlockable._id}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(405)
|
||||
expect(res.headers.allow).toBe(allowHeader)
|
||||
done()
|
||||
it 'allows admins to delete achievements using DELETE', (done) ->
|
||||
loginAdmin ->
|
||||
request.post {uri: url, json: unlockable2}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
unlockable2._id = body._id
|
||||
|
||||
request.del {uri: url + '/' + unlockable2._id}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(204)
|
||||
|
||||
request.del {uri: url + '/' + unlockable2._id}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(404)
|
||||
done()
|
||||
|
||||
it 'get schema', (done) ->
|
||||
request.get {uri: url + '/schema'}, (err, res, body) ->
|
||||
|
|
Loading…
Reference in a new issue