Added a new 'artisan' permission, which can edit most of the things admins can, but with less uncomfortable ultimate power.

This commit is contained in:
Nick Winter 2015-02-25 18:41:39 -08:00
parent 7e889a2a80
commit df4b98a9bf
24 changed files with 41 additions and 36 deletions

View file

@ -34,7 +34,7 @@ class LiveEditingMarkup extends TreemaNode.nodeMap.ace
valEl.append($('<div class="preview"></div>').hide())
addImageUpload: (valEl) ->
return unless me.isAdmin()
return unless me.isAdmin() or me.isArtisan()
valEl.append(
$('<div class="pick-image-button"></div>').append(
$('<button>Pick Image</button>')

View file

@ -5,6 +5,7 @@ module.exports = class Achievement extends CocoModel
@className: 'Achievement'
@schema: require 'schemas/models/achievement'
urlRoot: '/db/achievement'
editableByArtisans: true
isRepeatable: ->
@get('proportionalTo')?
@ -12,7 +13,7 @@ module.exports = class Achievement extends CocoModel
getExpFunction: ->
func = @get('function', true)
return utils.functionCreators[func.kind](func.parameters) if func.kind of utils.functionCreators
save: ->
@populateI18N()
super(arguments...)

View file

@ -5,3 +5,4 @@ module.exports = class Article extends CocoModel
@schema: require 'schemas/models/article'
urlRoot: '/db/article'
saveBackups: true
editableByArtisans: true

View file

@ -233,6 +233,7 @@ class CocoModel extends Backbone.Model
# actor is a User object
actor ?= me
return true if actor.isAdmin()
return true if actor.isArtisan() and @editableByArtisans
for permission in (@get('permissions', true) ? [])
if permission.target is 'public' or actor.get('_id') is permission.target
return true if permission.access in ['owner', 'read']
@ -243,6 +244,7 @@ class CocoModel extends Backbone.Model
# actor is a User object
actor ?= me
return true if actor.isAdmin()
return true if actor.isArtisan() and @editableByArtisans
for permission in (@get('permissions', true) ? [])
if permission.target is 'public' or actor.get('_id') is permission.target
return true if permission.access in ['owner', 'write']

View file

@ -10,6 +10,7 @@ module.exports = class Level extends CocoModel
'dungeons-of-kithgard': '5411cb3769152f1707be029c'
'defense-of-plainswood': '541b67f71ccc8eaae19f3c62'
urlRoot: '/db/level'
editableByArtisans: true
serialize: (supermodel, session, otherSession, cached=false) ->
o = @denormalize supermodel, session, otherSession # hot spot to optimize

View file

@ -17,6 +17,7 @@ module.exports = class LevelComponent extends CocoModel
@MissileID: '524cc2593ea855e0ab000142'
@FindsPathsID: '52872b0ead92b98561000002'
urlRoot: '/db/level.component'
editableByArtisans: true
set: (key, val, options) ->
if _.isObject key

View file

@ -5,6 +5,7 @@ module.exports = class LevelSystem extends CocoModel
@className: 'LevelSystem'
@schema: require 'schemas/models/level_system'
urlRoot: '/db/level.system'
editableByArtisans: true
set: (key, val, options) ->
if _.isObject key

View file

@ -27,6 +27,7 @@ module.exports = class ThangType extends CocoModel
'simple-boots': '53e237bf53457600003e3f05'
urlRoot: '/db/thang.type'
building: {}
editableByArtisans: true
initialize: ->
super()

View file

@ -12,6 +12,7 @@ module.exports = class User extends CocoModel
notyErrors: false
isAdmin: -> 'admin' in @get('permissions', true)
isArtisan: -> 'artisan' in @get('permissions', true)
isInGodMode: -> 'godmode' in @get('permissions', true)
isAnonymous: -> @get('anonymous', true)
displayName: -> @get('name', true)

View file

@ -1,7 +1,7 @@
extends /templates/base
block content
if me.isAdmin()
if !unauthorized
ol.breadcrumb
li
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
@ -13,7 +13,7 @@ block content
button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-all-button Recalculate All
button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate
button.achievement-tool-button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete
button.achievement-tool-button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save
button.achievement-tool-button(data-i18n="common.save", disabled=(me.isAdmin() === true || me.isArtisan() === true) ? undefined : "true").btn.btn-primary#save-button Save
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
span

View file

@ -1,4 +1,4 @@
button.btn.btn-primary#new-achievement-button(disabled=me.isAdmin() === true ? undefined : "true" data-i18n="editor.new_achievement_title") Create a New Achievement
button.btn.btn-primary#new-achievement-button(disabled=(me.isAdmin() === true || me.isArtisan() === true) ? undefined : "true" data-i18n="editor.new_achievement_title") Create a New Achievement
if !achievements.models.length
.panel

View file

@ -11,7 +11,7 @@ nav.navbar.navbar-default(role='navigation')
a(href="#system-patches" data-toggle="tab" data-i18n="resources.patches")#system-patches-tab Patches
ul.nav.navbar-nav.navbar-right
if !me.isAdmin()
if !me.isAdmin() && !me.isArtisan()
li#patch-system-button
a(data-i18n="[title]common.submit_patch")
span.glyphicon-floppy-disk.glyphicon

View file

@ -52,7 +52,7 @@ block header
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header(data-i18n="common.actions") Actions
li(class=!me.isAdmin() ? "disabled": "")
li(class=!me.isAdmin() && !me.isArtisan() ? "disabled": "")
a(data-i18n="common.fork")#fork-start-button Fork
li(class=!authorized ? "disabled": "")
a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert", disabled=!authorized)#revert-button Revert

View file

@ -83,8 +83,8 @@ else
button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
if me.get('anonymous') === false || me.get('iosIdentifierForVendor') || isIPadApp
button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
if me.isAdmin()
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
else if me.get('anonymous', true)
button.btn.settings(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="[title]play.settings")

View file

@ -251,7 +251,7 @@ module.exports = class CocoView extends Backbone.View
@_lastLoading = null
showReadOnly: ->
return if me.isAdmin()
return if me.isAdmin() or me.isArtisan()
warning = $.i18n.t 'editor.read_only_warning2', defaultValue: 'Note: you can\'t save any edits here, because you\'re not logged in.'
noty text: warning, layout: 'center', type: 'information', killer: true, timeout: 5000

View file

@ -51,7 +51,7 @@ module.exports = class AchievementEditView extends RootView
getRenderData: (context={}) ->
context = super(context)
context.achievement = @achievement
context.authorized = me.isAdmin()
context.authorized = me.isAdmin() or me.isArtisan()
context
afterRender: ->

View file

@ -15,6 +15,6 @@ module.exports = class AchievementSearchView extends SearchView
context.currentNewSignup = 'editor.new_achievement_title_login'
context.currentSearch = 'editor.achievement_search_title'
context.newModelsAdminOnly = true
context.unauthorized = true unless me.isAdmin()
context.unauthorized = true unless me.isAdmin() or me.isArtisan()
@$el.i18n()
context

View file

@ -22,18 +22,18 @@ class AchievementHandler extends Handler
'i18n'
'i18nCoverage'
]
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
jsonSchema = require '../../app/schemas/models/achievement.coffee'
hasAccess: (req) ->
req.method in ['GET', 'PUT'] or req.user?.isAdmin()
req.method in ['GET', 'PUT'] or req.user?.isAdmin() or req.user?.isArtisan()
hasAccessToDocument: (req, document, method=null) ->
method = (method or req.method).toLowerCase()
return true if method is 'get'
return true if req.user?.isAdmin()
return true if req.user?.isAdmin() or req.user?.isArtisan()
return true if method is 'put' and @isJustFillingTranslations(req, document)
return
@ -49,7 +49,7 @@ class AchievementHandler extends Handler
super req, res
delete: (req, res, slugOrID) ->
return @sendForbiddenError res unless req.user?.isAdmin()
return @sendForbiddenError res unless req.user?.isAdmin() or req.user?.isArtisan()
@getDocumentForIdOrSlug slugOrID, (err, document) => # Check first
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless document?

View file

@ -10,7 +10,7 @@ ArticleHandler = class ArticleHandler extends Handler
req.method is 'GET' or req.user?.isAdmin()
hasAccessToDocument: (req, document, method=null) ->
return true if req.method is 'GET' or method is 'get' or req.user?.isAdmin()
return true if req.method is 'GET' or method is 'get' or req.user?.isAdmin() or req.user?.isArtisan()
return false
module.exports = new ArticleHandler()

View file

@ -21,7 +21,7 @@ LevelSystemHandler = class LevelSystemHandler extends Handler
props
hasAccess: (req) ->
req.method is 'GET' or req.user?.isAdmin()
req.method is 'GET' or req.user?.isAdmin() or req.user?.isArtisan()
module.exports = new LevelSystemHandler()

View file

@ -43,7 +43,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler
hasAccessToDocument: (req, document, method=null) ->
method = (method or req.method).toLowerCase()
return true if method is 'get'
return true if req.user?.isAdmin()
return true if req.user?.isAdmin() or req.user?.isArtisan()
return true if method is 'post' and @isJustFillingTranslations(req, document)
return

View file

@ -42,6 +42,10 @@ UserSchema.methods.isAdmin = ->
p = @get('permissions')
return p and 'admin' in p
UserSchema.methods.isArtisan = ->
p = @get('permissions')
return p and 'artisan' in p
UserSchema.methods.isAnonymous = ->
@get 'anonymous'

View file

@ -1,4 +1,3 @@
config = require '../../../server_config'
require '../common'
@ -19,7 +18,7 @@ describe '/db/user, editing stripe.couponID property', ->
#- shared data between tests
joeData = null
firstSubscriptionID = null
it 'does not work for non-admins', (done) ->
loginJoe (joe) ->
joeData = joe.toObject()
@ -29,14 +28,14 @@ describe '/db/user, editing stripe.couponID property', ->
expect(res.statusCode).toBe(200) # fails silently
expect(res.body.stripe).toBeUndefined() # but still fails
done()
it 'does not work with invalid coupons', (done) ->
loginAdmin (admin) ->
joeData.stripe = { couponID: 'DNE' }
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(404)
done()
it 'sets the couponID on a user without an existing stripe object', (done) ->
joeData.stripe = { couponID: '20pct' }
request.put {uri: userURL, json: joeData }, (err, res, body) ->
@ -44,7 +43,7 @@ describe '/db/user, editing stripe.couponID property', ->
expect(res.statusCode).toBe(200)
expect(body.stripe.couponID).toBe('20pct')
done()
it 'just updates the couponID when it changes and there is no existing subscription', (done) ->
joeData.stripe.couponID = '500off'
request.put {uri: userURL, json: joeData }, (err, res, body) ->
@ -80,10 +79,10 @@ describe '/db/user, editing stripe.couponID property', ->
expect(res.statusCode).toBe(200)
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.discount).toBeDefined()
expect(customer.discount.coupon.id).toBe('500off')
expect(customer.discount?.coupon.id).toBe('500off')
done()
it 'updates the discount on the customer when an admin changes the couponID', (done) ->
loginAdmin (admin) ->
joeData.stripe.couponID = '20pct'
@ -93,7 +92,7 @@ describe '/db/user, editing stripe.couponID property', ->
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.discount.coupon.id).toBe('20pct')
done()
it 'removes discounts from the customer when an admin removes the couponID', (done) ->
delete joeData.stripe.couponID
request.put {uri: userURL, json: joeData }, (err, res, body) ->
@ -111,4 +110,3 @@ describe '/db/user, editing stripe.couponID property', ->
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.discount.coupon.id).toBe('20pct')
done()

View file

@ -71,13 +71,13 @@ describe '/db/patch', ->
it 'does not add duplicate watchers', (done) ->
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: watchingURL, json: {on: true}}, (err, res, body) ->
expect(body.watchers.length).toBe(4)
expect(body.watchers.length).toBe(3)
done()
it 'allows removing yourself', (done) ->
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: watchingURL, json: {on: false}}, (err, res, body) ->
expect(body.watchers.length).toBe(3)
expect(body.watchers.length).toBe(2)
done()
it 'allows the submitter to withdraw the pull request', (done) ->
@ -157,9 +157,3 @@ describe '/db/patch', ->
Patch.findOne({}).exec (err, article) ->
expect(article.get('status')).toBe 'accepted'
done()