Added achievement deleting and automatic achievement filling

This commit is contained in:
Ruben Vereecken 2014-08-08 17:14:57 +02:00
parent 3bfd341363
commit 47f00f9b5e
15 changed files with 182 additions and 50 deletions

View file

@ -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

View file

@ -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

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") 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

View file

@ -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}"

View file

@ -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

View file

@ -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

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

@ -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

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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) ->

View file

@ -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)

View file

@ -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) ->