Compare commits

...

10 Commits

Author SHA1 Message Date
Torrie Fischer
61c39dc9c2 wip? 2023-06-10 13:21:31 +02:00
Trever Fischer
4ac2d61359 Implement new event marshalling system 2012-11-21 22:09:35 -05:00
Trever Fischer
8ef29f3358 crash-- 2012-11-18 09:21:16 -05:00
Trever Fischer
f9340c7582 debug-- 2012-11-18 09:21:09 -05:00
Trever Fischer
ec6cc0db7d Fix event dispatching and attempt to reconnect on poll error 2012-11-18 09:20:53 -05:00
Trever Fischer
25ae5444f7 Use None where appropriate 2012-11-18 09:20:20 -05:00
Trever Fischer
3dea38f51d Send multiple events to web polls and fix server events 2012-11-18 09:20:04 -05:00
Trever Fischer
018b360f61 Fix up the event_stats command and include web queue data 2012-11-18 09:19:18 -05:00
Trever Fischer
adde1343f1 Fix web queues not always being present in beanstalkd, meaning dropped events 2012-11-18 09:18:51 -05:00
Trever Fischer
690ff8f21e Support handling vault-content events in the vault UI 2012-11-17 21:33:42 -05:00
28 changed files with 1318 additions and 451 deletions

82
api/event-types.yml Normal file
View File

@@ -0,0 +1,82 @@
---
routes:
quit: [api, web]
join: [api, web]
broadcast: [server, web]
chat: [web]
vault-contents: [server, web]
player-death: [api, web]
server-heartbeat: [api ]
web-heartbeat: [web]
player-kick: [server, web]
player-message: [server, web]
market-order: [market]
block-break: [api]
block-place: [api]
player-murder: [api]
weather: [api, web]
test: [api, web, server]
types:
vault-slot:
material: int
quantity: int
damage: int
data: int
position: int
name: !optional string
durability: !optional int
events:
test:
data: !optional string
quit:
player: string
join:
player: string
broadcast:
message: string
chat:
message: string
sender: string
vault-contents:
player: string
items:
- vault-slot
player-death:
player: string
message: string
server-heartbeat:
port: int
name: string
worldTimes: dict
web-heartbeat:
server: int
heartbeat: dict
time: int
day-period: string
player-kick:
player: string
message: string
player-message:
player: string
message: string
market-order:
orderID: int
block-break:
player: string
x: int
y: int
z: int
material: int
block-place:
player: string
x: int
y: int
z: int
material: int
player-murder:
player: string
killer: string
message: string
weather:
world: string
isRaining: boolean

View File

@@ -1,155 +1,358 @@
from django.conf import settings
import os
from django.core.cache import cache
import time
from minecraft.models import Server
from json import loads, dumps, JSONEncoder
from json import loads, dumps
import beanstalkc
from django.contrib.auth.models import User
import yaml
import django.dispatch
class EventEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Event):
return {'type': obj.type, 'payload': obj.data}
return super(EventEncoder, self).default(obj)
on_event = django.dispatch.Signal(providing_args=["event"])
def cache_heartbeat(sender, event, *args, **kwargs):
#cache.set('caminus-server-heartbeat-%s'%(request.server.id), evt['payload'], 86400)
pass
on_event.connect(cache_heartbeat, sender=intern('heartbeat'))
class Dispatcher(object):
@classmethod
def routes(cls):
types = cache.get('caminus-event-routes')
types = None
if types is None:
typefile = os.path.sep.join(
__file__.split(os.path.sep)[0:-1]+
['event-types.yml',]
)
yaml.add_constructor(r'!optional', OptionalEventProperty)
types = yaml.load(open(typefile, 'r'))['routes']
cache.set('caminus-event-routes', types, 3600)
return types
@classmethod
def dispatch(cls, event):
routes = cls.routes()
if event._type in routes:
for route in routes[event._type]:
if route == "api":
on_event.send_robust(sender=intern(event._type), event=event)
elif route == "web":
WebQueue.route(event)
elif route == "server":
ServerQueue.route(event)
elif route == "market":
MarketQueue.route(event)
else:
raise KeyError, "Unknown route %s"%route
else:
raise KeyError, "Unable to find route for %s event"%(event._type)
@classmethod
def broadcast(cls, msg, *args):
cls.dispatch(Event('broadcast', message=msg%args))
@classmethod
def user_message(cls, user, message, args):
player = user.minecraftprofile.mc_username
cls.player_message(player, message, *args)
@classmethod
def player_message(cls, playername, message, *args):
message = message % args
cls.dispatch(Event('player-message', player=playername,
message=message))
@classmethod
def chat(cls, playername, message, *args):
cls.dispatch(Event('chat', player=playername, message=message%args))
class Event(object):
def __init__(self, type, data):
self.type = type
self.data = data
def __init__(self, type=None, *args, **kwargs):
self._id = -1
self._stamp = 0
self._conn = None
self._properties = {}
types = self._types()
if type in types:
self._type = type
self._properties = self._marshallType(kwargs, types[type], self._datatypes(), type)
else:
raise ValueError, "Unknown event type '%s'"%(type)
class ChatEvent(Event):
def __init__(self, sender, message):
super(ChatEvent, self).__init__(type='chat', data={'sender': sender,
'message': message})
def _delete(self):
self._conn.delete(self._id)
class VaultContentsEvent(Event):
def __init__(self, player, items):
super(VaultContentsEvent, self).__init__(type='vault-contents',
data={'player': player, 'items': items})
def _bury(self, priority=0):
self._conn.bury(self._id, 0)
class QuitEvent(Event):
def __init__(self, player):
super(QuitEvent, self).__init__(type='quit', data={'player': player})
def __getattribute__(self, name):
if name.startswith('_'):
return super(Event, self).__getattribute__(name)
return self._properties[name]
class JoinEvent(Event):
def __init__(self, player):
super(JoinEvent, self).__init__(type='join', data={'player': player})
def __setattr__(self, name, value):
if name.startswith('_'):
return super(Event, self).__setattr__(name, value)
if name in self._properties:
raise NotImplementedError, "Event properties are not modifyable"
class BroadcastEvent(Event):
def __init__(self, message):
super(BroadcastEvent, self).__init__(type='broadcast', data={'message':
message})
@classmethod
def _marshallType(cls, data, typeDescription, dataTypes, paramPath=""):
assert(isinstance(dataTypes, dict))
if data is None:
return data
if isinstance(typeDescription, OptionalEventProperty):
return cls._marshallType(data, str(typeDescription), dataTypes, paramPath)
try:
if typeDescription == "string":
return str(data)
if typeDescription == "int":
return int(data)
if typeDescription == "dict":
return dict(data)
if typeDescription == "list":
return list(data)
except TypeError, e:
raise ValueError, "Could not convert '%s' to %s type for %s"%(data,
typeDescription, paramPath)
if isinstance(typeDescription, dict):
ret = {}
for key, elementType in typeDescription.iteritems():
key = key.replace('-', '_')
if key in data:
if key.startswith("_"):
raise ValueError, "Parameter '%s.%s' cannot start with '_'"%(key, paramPath)
ret[key] = cls._marshallType(data[key], elementType, dataTypes,
"%s.%s"%(paramPath, key))
elif not isinstance(elementType, OptionalEventProperty):
raise KeyError, "Missing parameter '%s.%s'"%(paramPath, key)
else:
ret[key] = None
return ret
if isinstance(typeDescription, list):
ret = []
i = 0
for item in data:
ret.append(cls._marshallType(item, typeDescription[0], dataTypes, "%s[%d]"%(paramPath, i)))
i += 1
return ret
if typeDescription in dataTypes:
return cls._marshallType(data, dataTypes[typeDescription], dataTypes, paramPath)
raise TypeError, "Unknown event data type '%s.%s'"%(paramPath, typeDescription)
class PlayerDeathEvent(Event):
def __init__(self, player, message):
super(PlayerDeathEvent, self).__init__(type='player-death',
data={'player': player, 'message': message})
@classmethod
def _types(cls):
types = cache.get('caminus-event-types')
types = None
if types is None:
typefile = os.path.sep.join(
__file__.split(os.path.sep)[0:-1]+
['event-types.yml',]
)
yaml.add_constructor(r'!optional', OptionalEventProperty)
types = yaml.load(open(typefile, 'r'))['events']
cache.set('caminus-event-types', types, 3600)
return types
class ServerHeartbeatEvent(Event):
def __init__(self, server, data):
now = server.current_time()
t = now.second+now.minute*60+now.hour*60*60
super(ServerHeartbeatEvent, self).__init__(type='server-heartbeat',
data={'server': server.id, 'heartbeat': data, 'time': t,
'day-period': server.day_period()})
@classmethod
def _datatypes(cls):
typefile = os.path.sep.join(
__file__.split(os.path.sep)[0:-1]+
['event-types.yml',]
)
yaml.add_constructor(r'!optional', OptionalEventProperty)
types = yaml.load(open(typefile, 'r'))['types']
return types
class KickEvent(Event):
def __init__(self, user, message):
super(KickEvent, self).__init__(type='player-kick',
data={'player': user, 'message': message})
def __repr__(self):
return "Event(%r)"%(self._properties)
class PlayerMessageEvent(Event):
def __init__(self, user, message):
super(PlayerMessageEvent, self).__init__(type='player-message',
data={'message': message, 'player': user})
def __eq__(self, other):
return self._properties == other._properties and self._id == other._id
class MarketOrderEvent(Event):
def __init__(self, orderID):
super(MarketOrderEvent, self).__init__(type='market-order',
data={'orderID': orderID})
@classmethod
def _fromBeanstalkJob(cls, job):
assert(isinstance(job, beanstalkc.Job))
id = job.jid
evt = cls._fromJSON(job.body)
evt._id = int(id)
evt._conn = job.conn
return evt
def server_queue(server, users=[]):
queueName = 'caminus-broadcast-%s'%server.id
queue = beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
def _toDict(self):
ret = {
'_id': self._id,
'_type': self._type,
'_stamp': self._stamp,
}
ret.update(self._properties)
return ret
@classmethod
def _fromDict(cls, data):
assert(isinstance(data, dict))
type = data['_type']
properties = {}
for prop in data:
if prop.startswith('_'):
continue
properties[prop] = data[prop]
evt = Event(type, **properties)
if '_id' in data:
evt._id = int(data['_id'])
return evt
def _toJSON(self):
return dumps(self._toDict())
@classmethod
def _fromJSON(cls, json):
assert(isinstance(json, str))
return cls._fromDict(loads(json))
class OptionalEventProperty(str):
def __new__(cls, loader=None, node=None):
if node is None:
# Used with OptionalEventProperty("some-prop-name"), such as with pickling
return str.__new__(cls, loader)
# Used with yaml loader
return str.__new__(cls, loader.construct_scalar(node))
class EventQueue(object):
def __init__(self, name):
self.name = name
self.queue = beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
port=settings.CAMINUS_BEANSTALKD_PORT)
queue.use(queueName)
queue.watch(queueName)
if len(users) > 0:
for user in users:
queue.watch("caminus-user-%s"%user)
return queue
self.queue.use(self.name)
self.queue.watch(self.name)
def send_server_event(server, event):
if settings.CAMINUS_USE_BEANSTALKD:
queue = server_queue(server)
json = dumps(event, cls=EventEncoder)
queue.put(json)
@classmethod
def route(self, evt):
raise NotImplementedError
def broadcast_server_event(event):
def sendEvent(self, evt):
assert(isinstance(evt, Event))
id = self.queue.put(evt._toJSON())
def getEvents(self, timeout=30):
ret = []
job = self.queue.reserve(timeout)
while job:
ret.append(job)
job = self.queue.reserve(timeout=0)
return map(lambda x: Event._fromBeanstalkJob(x), ret)
@classmethod
def flushAll(cls):
for q in cls.allQueues():
q.flush()
def flush(self):
job = self.queue.peek_ready()
while job:
job.delete()
job = self.queue.peek_ready()
@staticmethod
def connection():
return beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
port=settings.CAMINUS_BEANSTALKD_PORT)
@classmethod
def allQueues(cls):
c = cls.connection()
ret = []
for c in c.tubes():
ret.append(EventQueue(c))
return ret
class MarketQueue(EventQueue):
def __init__(self):
super(MarketQueue, self).__init__('caminus-market')
@classmethod
def route(cls, evt):
cls().sendEvent(evt)
class ServerQueue(EventQueue):
def __init__(self, server):
super(ServerQueue, self).__init__('caminus-broadcast-%s'%server.id)
@classmethod
def route(cls, evt):
cls.broadcast(evt)
@classmethod
def broadcast(cls, evt):
for server in Server.objects.all():
send_server_event(server, event)
queue = ServerQueue(server)
queue.sendEvent(evt)
def server_broadcast(message, *args):
message = message%args
for server in Server.objects.all():
event = BroadcastEvent(message)
send_server_event(server, event)
send_web_event(event)
class WebQueue(EventQueue):
def __init__(self, id):
self.id = id
super(WebQueue, self).__init__('caminus-web-%s'%(id))
if id != "0":
activeCache = cache.get('minecraft-web-queues')
if activeCache is None:
activeCache = {}
activeCache[id] = time.time()
expired = []
for queueName, stamp in activeCache.iteritems():
if time.time()-stamp > 120:
expired.append(queueName)
for e in expired:
del activeCache[e]
cache.set('minecraft-web-queues', activeCache, 3600)
for e in expired:
q = WebQueue(e)
q.flush()
def user_message(user, message, *args):
player = user.minecraftprofile.mc_username
player_message(player, message, *args)
@classmethod
def route(cls, event):
cls.broadcast(event)
def player_message(playername, message, *args):
message = message%args
for server in Server.objects.all():
event = PlayerMessageEvent(playername, message)
send_server_event(server, event)
@classmethod
def broadcast(cls, event):
latest = cache.get('minecraft-web-events')
if latest is None:
latest = []
latest.append(dumps(event._toJSON()))
while len(latest) > 10:
latest.pop(0)
cache.set('minecraft-web-events', latest, 86400)
def web_queue(id):
queueName = 'caminus-web-%s'%id
queue = beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
port = settings.CAMINUS_BEANSTALKD_PORT)
queue.use(queueName)
queue.watch(queueName)
return queue
for queue in cls.activeQueues():
queue.sendEvent(event)
def market_queue():
queueName = 'caminus-market'
queue = beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
port = settings.CAMINUS_BEANSTALKD_PORT)
queue.use(queueName)
queue.watch(queueName)
return queue
@staticmethod
def activeQueues():
activeCache = cache.get('minecraft-web-queues')
if activeCache is None:
activeCache = {}
ret = []
for name in activeCache.iterkeys():
ret.append(WebQueue(name))
return ret
def queue_market_event(event):
queue = market_queue()
json = dumps({'stamp': time.time(), 'event': event}, cls=EventEncoder)
queue.put(json)
@staticmethod
def clearActiveQueues():
cache.set('minecraft-web-queues', None)
def send_web_event(event):
latest = cache.get('minecraft-web-events')
if latest is None:
latest = []
latest.append(dumps(event, cls=EventEncoder))
while len(latest) > 10:
latest.pop(0)
cache.set('minecraft-web-events', latest, 86400);
if settings.CAMINUS_USE_BEANSTALKD:
queue = beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
port = settings.CAMINUS_BEANSTALKD_PORT)
json = dumps({'stamp': time.time(), 'event':event}, cls=EventEncoder)
for tube in queue.tubes():
if tube.startswith("caminus-web-"):
queue.use(tube)
pendingJob = queue.peek_ready()
if pendingJob:
pending = loads(pendingJob.body)
if time.time()-pending['stamp'] > 30:
pendingJob.delete()
queue.put(json)
def chat(playername, message):
evt = ChatEvent(playername, message)
send_web_event(evt)
def getEvents(self, timeout=30):
if self.id == "0":
latestEvents = cache.get('minecraft-web-events')
if latestEvents is None:
latestEvents = []
ret = []
for e in latestEvents:
ret.append(loads(e))
return ret
else:
ret = super(WebQueue, self).getEvents(timeout)
for evt in ret:
evt._delete()
return ret

View File

@@ -5,14 +5,14 @@ from django.conf import settings
import appversion
from minecraft.models import MinecraftProfile
from local.models import Quote
from minecraft.models import MOTD, Server, PlayerSession
from minecraft.models import MOTD, Server, PlayerSession, Item
from django.db.models import F
from django.http import HttpResponse
from urllib2 import urlopen
import json
from datetime import datetime
from models import cachePlayerList
from events import server_queue, web_queue, chat, server_broadcast, send_web_event, QuitEvent, JoinEvent, PlayerDeathEvent, ServerHeartbeatEvent
from events import ServerQueue, WebQueue, Event, Dispatcher
from bounty.models import Bounty
from vault.models import VaultSlot
@@ -44,7 +44,7 @@ class NewPlayerSessionHandler(BaseHandler):
server = request.server
profile = MinecraftProfile.objects.get(mc_username__exact=playername)
session = PlayerSession.objects.create(server=server, player=profile, ip=ip)
send_web_event(JoinEvent(playername))
WebQueue.broadcast(Event('join', player=playername))
return {'success': True, 'error': '', 'permissions': profile.serverPermissions(), 'sessionId': session.id}
else:
return {'success': False, 'error': 'Your account is inactive.', 'permissions': []}
@@ -57,7 +57,7 @@ class ClosePlayerSessionHandler(BaseHandler):
for session in sessions:
session.end = datetime.now()
session.save()
send_web_event(QuitEvent(playername))
WebQueue.broadcast(Event('quit', player=playername))
return {'valid': True}
class EconomyHandler(BaseHandler):
@@ -88,42 +88,26 @@ class ServerEventHandler(BaseHandler):
allowed_methods = ('GET', 'POST', 'PUT')
def read(self, request):
queue = server_queue(request.server)
queue.watch('caminus-broadcast-%s'%request.server.id)
events = []
job = queue.reserve(timeout=30)
while job:
job.bury()
events.append({'id': job.jid, 'event': json.loads(job.body)})
job = queue.reserve(timeout=0)
return {'events': events, 'is-live': settings.CAMINUS_USE_BEANSTALKD}
queue = ServerQueue(request.server)
events = []
for e in queue.getEvents():
events.append(json.loads(e.body)['event'])
e.bury()
print {'events': events}
return {'events': events, 'is-live': settings.CAMINUS_USE_BEANSTALKD}
def create(self, request):
queue = server_queue(request.server)
try:
queue.delete(int(request.POST['job']))
except Exception, e:
pass
return {'result': 'success'}
queue = ServerQueue(request.server)
try:
queue.deleteJob(int(request.POST['job']))
except Exception, e:
pass
return {'result': 'success'}
def update(self, request):
events = json.loads(request.POST['events'])['events']
for evt in events:
if evt['type'] == 'chat':
chat(evt['payload']['sender'], evt['payload']['message'])
if evt['type'] == 'player-death':
send_web_event(PlayerDeathEvent(evt['payload']['player'],
evt['payload']['message']))
if evt['type'] == 'heartbeat':
cache.set('caminus-server-heartbeat-%s'%(request.server.id), evt['payload'], 86400)
send_web_event(ServerHeartbeatEvent(request.server, evt['payload']))
if evt['type'] == 'player-murder':
bounties = Bounty.objects.filter(target__mc_username=evt['payload']['player'])
killer = MinecraftProfile.objects.get(mc_username=evt['payload']['killer'])
for bounty in bounties:
bounty.close(killer)
if len(bounties) > 0:
server_broadcast("The bounty on %s has been collected."%(evt['payload']['player']))
Dispatcher.dispatch(Event.fromDict(evt)
return {'result': 'success'}
class ChatHandler(BaseHandler):
@@ -131,8 +115,8 @@ class ChatHandler(BaseHandler):
def create(self, request):
chat(request.user.minecraftprofile.mc_username, request.POST['message'])
server_broadcast("<%s> %s"%(request.user.minecraftprofile.mc_username,
request.POST['message']))
ServerQueue.broadcast(BroadcastEvent("<%s> %s",
request.user.minecraftprofile.mc_username, request.POST['message']))
class PollHandler(BaseHandler):
allowed_methods = ('GET',)
@@ -150,20 +134,8 @@ class PollHandler(BaseHandler):
pollData['events'] = []
pollData['info'] = info
pollData['poll-id'] = timestamp
if timestamp == "0" and settings.CAMINUS_USE_BEANSTALKD:
pollData['poll-id'] = time.time()
latestEvents = cache.get('minecraft-web-events')
if not latestEvents:
latestEvents = []
for e in latestEvents:
pollData['events'].append(json.loads(e))
elif settings.CAMINUS_USE_BEANSTALKD:
eventQueue = web_queue(timestamp)
event = eventQueue.reserve(timeout=30)
if event:
eventData = json.loads(event.body)
pollData['events'].append(eventData['event'])
event.delete()
eventQueue = WebQueue(timestamp)
pollData['events'] = eventQueue.getEvents()
return pollData
class VaultHandler(BaseHandler):
@@ -173,9 +145,17 @@ class VaultHandler(BaseHandler):
player = MinecraftProfile.objects.get(mc_username__exact=playername)
items = []
for slot in player.vault_slots.all():
items.append({'item': slot.item, 'quantity':
slot.quantity, 'damage': slot.damage, 'data': slot.data,
'position': slot.position})
data = {}
data['quantity'] = slot.quantity
data['position'] = slot.position
data['item'] = None
data['damage'] = 0
data['data'] = 0
if slot.item:
data['data'] = slot.item.data
data['item'] = slot.item.material
data['damage'] = slot.item.damage
items.append(data)
return {'items': items}
def update(self, request, playername):
@@ -186,18 +166,13 @@ class VaultHandler(BaseHandler):
updated = False
slot,created = VaultSlot.objects.get_or_create(player=player,
position=stack['position'])
if slot.item != stack['item']:
slot.item = stack['item']
item = Item.get(stack['item'], stack['damage'], stack['data'])
if slot.item != item:
slot.item = item
updated = True
if slot.quantity != stack['quantity']:
slot.quantity = stack['quantity']
updated = True
if slot.damage != stack['damage']:
slot.damage = stack['damage']
updated = True
if slot.data != stack['data']:
slot.data = stack['data']
updated = True
if updated:
slot.save()
return {'success': True}

View File

@@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand
from django.core.cache import cache
from api import events
from minecraft.models import Server
@@ -8,17 +9,23 @@ class Command(BaseCommand):
def handle(self, *args, **options):
servers = Server.objects.all()
for s in servers:
queue = events.server_queue(s)
stats = queue.stats()
queue = events.ServerQueue(s)
stats = queue.queue.stats()
print s
for k,v in stats.iteritems():
print "\t%s: %s"%(k, v)
print "\tTubes:"
for t in queue.tubes():
print "\t\t%s"%(t)
queue.use(t)
next = queue.peek_ready()
if next:
print "\t\t\tNext job: %s"%(next.body)
else:
print "\t\t\tNo pending job."
print "Tubes:"
for t in queue.queue.tubes():
print "\t%s"%(t)
queue.queue.use(t)
next = queue.queue.peek_ready()
if next:
print "\t\tNext job: %s"%(next.body)
else:
print "\t\tNo pending job."
print "Web queues:"
webQueues = cache.get("minecraft-web-queues")
if webQueues is None:
webQueues = {}
for queue,stamp in webQueues.iteritems():
"\t%s: %d"%(queue, stamp)

View File

@@ -0,0 +1,140 @@
from django.core.management.base import BaseCommand
from django.core.cache import cache
from api import events
from pprint import pprint
from optparse import make_option
_t = type
class Command(BaseCommand):
help = 'Display information about available event types'
option_list = BaseCommand.option_list + (
make_option('--header',
help='Header to append to each file'),
make_option('--output',
help='Directory to write files to',
default='.'),
make_option('--lang',
help='Language to export to'),
)
def handle(self, *args, **options):
if options['lang'] == 'java':
if options['output']:
outdir = options['output']
else:
outdir = '.'
header = ""
if options['header']:
fh = open(options['header'], 'r')
header = fh.read()
for typename, properties in events.Event.types().iteritems():
outfile = open(outdir+'/'+self.javaName(typename)+"Event.java", 'w')
outfile.write(header)
outfile.write("import org.json.*;\n")
outfile.write("import java.util.*;\n")
outfile.write("public class %sEvent extends Event {\n"%(self.javaName(typename)))
for propname,type in properties.iteritems():
outfile.write("\tpublic %s %s;\n"%(self.javaType(type), self.javaName(propname, False)))
outfile.write("\tpublic static %sEvent fromJSON(JSONObject obj) throws JSONException {\n"%(self.javaName(typename)))
outfile.write("\t\t%sEvent ret = new %sEvent();\n"%(self.javaName(typename), self.javaName(typename)))
for propname, type in properties.iteritems():
if isinstance(type, events.OptionalEventProperty):
outfile.write("\t\ttry {\n\t")
outfile.write("\t\t"+self.jsonCode(propname, type)+"\n");
if isinstance(type, events.OptionalEventProperty):
outfile.write("\t\t} catch (JSONException e) {}\n")
outfile.write("\t\treturn ret;\n")
outfile.write("\t}\n")
outfile.write("\tpublic JSONWriter toJSON(JSONWriter writer) throws JSONException {\n")
for propname, type in properties.iteritems():
outfile.write("writer.key(\"%s\").value(this.%s);\n"%(propname, self.javaName(propname, False)))
outfile.write("\t\treturn writer;\n")
outfile.write("\t}\n")
outfile.write("\tpublic String jsonName() { return \"%s\"; }\n"%(typename))
outfile.write("\tstatic { Event.registerHandler(\"%s\", %sEvent.class); }\n"%(typename, self.javaName(typename)))
outfile.write("}\n")
outfile.close()
print "Wrote", self.javaName(typename)+"Event.java"
for typename, properties in events.Event.datatypes().iteritems():
outfile = open(outdir+'/'+self.javaName(typename)+".java", 'w')
outfile.write(header)
outfile.write("import org.json.*;\n")
outfile.write("import java.util.*;\n")
outfile.write("public class %s {\n"%(self.javaName(typename)))
for propname, type in properties.iteritems():
outfile.write("\tpublic %s %s;\n"%(self.javaType(type), self.javaName(propname, False)))
outfile.write("\tpublic static %s fromJSON(JSONObject obj) throws JSONException {\n"%(self.javaName(typename)))
outfile.write("\t\t%s ret = new %s();\n"%(self.javaName(typename), self.javaName(typename)))
for propname, type in properties.iteritems():
if isinstance(type, events.OptionalEventProperty):
outfile.write("\t\ttry {\n\t")
outfile.write("\t\t"+self.jsonCode(propname, type)+"\n");
if isinstance(type, events.OptionalEventProperty):
outfile.write("\t\t} catch (JSONException e) {}\n")
outfile.write("\t\treturn ret;\n")
outfile.write("\t}\n")
outfile.write("}\n")
outfile.close()
print "Wrote", self.javaName(typename)+".java"
else:
print 'Unknown language', options['lang']
@staticmethod
def javaName(name, isClass=True):
name = ''.join(map(lambda x:x[0].upper()+x[1:], name.split('-')))
if not isClass:
name = name[0].lower()+name[1:]
return ''.join(name)
@classmethod
def jsonCode(cls, propname, type):
propname = cls.javaName(propname, False)
tmpl = "ret.%s = obj.%s(\"%s\");"
if type == "string":
return tmpl%(propname, "getString", propname)
if type == "int":
return tmpl%(propname, "getInt", propname)
if type == "boolean":
return tmpl%(propname, "getBoolean", propname)
if type == "dict":
return """
JSONObject %s_obj = obj.getJSONObject("%s");
for(String name : JSONObject.getNames(%s_obj)) {
ret.%s.put(name, %s_obj.get(name));
}
"""%(propname, propname, propname, propname, propname)
if isinstance(type, str):
return "ret.%s = %s.fromJSON(obj.getJSONObject(%s));"%(propname,
cls.javaName(type), propname)
if isinstance(type, list):
return """
JSONArray %s_list = obj.getJSONArray("%s");
ret.%s = new ArrayList<%s>();
for(int i = 0;i<%s_list.length();i++) {
ret.%s.add(%s.fromJSON(%s_list.getJSONObject(i)));
}"""%(
propname, propname, propname, cls.javaName(type[0]), propname, propname,
cls.javaName(type[0]), propname
)
raise TypeError, "Cannot convert %s to java type from JSON."%(type)
@classmethod
def javaType(cls, type):
if type == "string":
return "String"
if type == "int":
return "int"
if type == "list":
return "List<?>"
if type == "dict":
return "Map<String, Object>"
if type == "boolean":
return "boolean"
if isinstance(type, str):
return cls.javaName(type)
if isinstance(type, list):
return "List<%s>"%(cls.javaType(type[0]))
if isinstance(type, dict):
return "Map<String, Object>"
raise TypeError, "Cannot convert %s to java type."%(type)

View File

@@ -8,12 +8,12 @@ class Command(BaseCommand):
def handle(self, *args, **options):
servers = Server.objects.all()
for s in servers:
queue = events.server_queue(s)
stats = queue.stats()
for t in queue.tubes():
queue.use(t)
job = queue.peek_ready()
queue = events.ServerQueue(s)
stats = queue.queue.stats()
for t in queue.queue.tubes():
queue.queue.use(t)
job = queue.queue.peek_ready()
while job:
print "Deleting %s from %s: %s"%(job.jid, t, job.body)
job.delete()
job = queue.peek_ready()
job = queue.queue.peek_ready()

252
api/oldtests.py Normal file
View File

@@ -0,0 +1,252 @@
from django.utils import unittest
import json
from django.test.client import Client
from django.contrib.auth.models import User
from minecraft.models import MinecraftProfile, Server, PlayerSession, MOTD, Ban
from local.models import Quote
import hashlib
import events
from django.conf import settings
class EventDispatcherTest(unittest.TestCase):
def setUp(self):
self.server = Server.objects.create(hostname='localhost', secret='secret')
events.EventQueue.flushAll()
def tearDown(self):
self.server.delete()
events.EventQueue.flushAll()
def testBroadcast(self):
webQueue = events.WebQueue(1)
serverQueue = events.ServerQueue(self.server)
events.Dispatcher.dispatch(events.Event('broadcast', message="Test"))
webEvents = webQueue.getEvents()
serverEvents = serverQueue.getEvents()
print webEvents
self.assertEqual(len(webEvents), 1)
self.assertEqual(len(serverEvents), 1)
self.assertEqual(webEvents[0]._type, 'broadcast')
self.assertEqual(serverEvents[0]._type, 'broadcast')
class EventQueueTest(unittest.TestCase):
def setUp(self):
self.queue = events.EventQueue("test")
self.queue.flush()
def tearDown(self):
self.queue.flush()
def testSendEvent(self):
self.queue.sendEvent(events.Event('test'))
def testSendReceiveEvent(self):
self.queue.sendEvent(events.Event('test'))
self.assertEqual(len(self.queue.getEvents()), 1)
def testMarshallEvent(self):
sentEvent = events.Event('test')
self.queue.sendEvent(sentEvent)
recvEvent = self.queue.getEvents()[0]
self.assertEqual(type(sentEvent), type(recvEvent))
self.assertEqual(sentEvent._type, recvEvent._type)
self.assertNotEqual(sentEvent._id, recvEvent._id)
class EventMarshallTest(unittest.TestCase):
def testMarshallToValidJSON(self):
json.loads(events.Event('test')._toJSON())
def testReprToValidJSON(self):
json.loads(repr(events.Event('test')))
def testLoadFromJSON(self):
evt = events.Event('test', player='')
jsonData = evt._toJSON()
decodedEvent = events.Event._fromJSON(jsonData)
class ServerPingTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
def tearDown(self):
self.user.delete()
self.server.delete()
def testPing(self):
resp = self.client.get('/api/server/whoami',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token))
self.assertEqual(resp.status_code, 200)
resp = json.loads(resp.content)
self.assertEqual(resp["api-version"], 2)
self.assertTrue("api-version" in resp)
class ServerEventTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
def tearDown(self):
self.user.delete()
self.server.delete()
def testBroadcast(self):
events.ServerQueue.broadcast(events.Event('broadcast', message="Test message"))
response = json.loads(self.client.get('/api/server/events',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertTrue(len(response['events']) > 0)
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertEqual(response['result'], 'success')
def testUserMessage(self):
events.user_message(self.user, "Test user message")
response = json.loads(self.client.get('/api/server/events',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertTrue(len(response['events']) > 0)
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertEqual(response['result'], 'success')
class MOTDTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.server = Server.objects.create(hostname="localhost", secret="")
def tearDown(self):
self.server.delete()
def testUnregisteredUser(self):
response = json.loads(self.client.get('/api/motd/NewUser').content)
self.assertIsInstance(response['motd'], list)
def testRandomMOTD(self):
m = MOTD.objects.create(server=self.server, text="testMOTD")
q = Quote.objects.create(text="testQUOTE")
response = json.loads(self.client.get('/api/motd/NewUser').content)
m.delete()
q.delete()
foundMOTD = False
foundQuote = False
for line in response['motd']:
if line == "testMOTD":
foundMOTD = True
if line == '"testQUOTE"':
foundQuote = True
self.assertTrue(foundMOTD)
self.assertTrue(foundQuote)
class SessionTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
def tearDown(self):
self.user.delete()
self.server.delete()
def testBannedStart(self):
b = Ban.objects.create(player=self.user.minecraftprofile, banner=self.user, reason="test")
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
b.delete()
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertFalse(session['success'])
def testBadUserStart(self):
resp = self.client.post('/api/server/session/SomeUnknownUser/new', {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertFalse(session['success'])
def testInactiveStart(self):
self.user.is_active = False
self.user.save()
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.user.is_active = True
self.user.save()
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertFalse(session['success'])
def testSessionStart(self):
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertTrue(session['success'])
def testSessionEnd(self):
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
session = json.loads(resp.content)
resp = self.client.get('/api/server/session/%s/close'%(self.user.minecraftprofile.mc_username), HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.assertEqual(resp.status_code, 200)
class PollTest(unittest.TestCase):
def setUp(self):
self.client = Client()
def testAnonymousPoll(self):
resp = self.client.get('/api/poll/0')
self.assertEqual(resp.status_code, 200)
data = json.loads(resp.content)
self.assertTrue('info' in data)
self.assertTrue('server' in data['info'])
self.assertTrue('user' in data['info'])
self.assertEqual(len(data['info']['user']), 0)
class EconomyTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.user.minecraftprofile.currencyaccount.balance=42
self.user.minecraftprofile.currencyaccount.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
def tearDown(self):
self.user.delete()
self.server.delete()
def testBalanceQuery(self):
resp = self.client.get('/api/server/economy/ValidUsername', HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertEqual(data['balance'], 42)
def testDeposit(self):
resp = self.client.put('/api/server/economy/ValidUsername', {'delta': 100}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertEqual(data['balance'], 142)
self.assertTrue(data['success'])
def testWithdraw(self):
resp = self.client.put('/api/server/economy/ValidUsername', {'delta': -40}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertEqual(data['balance'], 2)
self.assertTrue(data['success'])
def testOverdraw(self):
resp = self.client.put('/api/server/economy/ValidUsername', {'delta': -400}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertFalse(data['success'])

View File

@@ -1,196 +1,216 @@
from django.utils import unittest
import json
from django.test.client import Client
from django.contrib.auth.models import User
from django.core.cache import cache
from minecraft.models import MinecraftProfile, Server, PlayerSession, MOTD, Ban
from local.models import Quote
import hashlib
import events
from django.conf import settings
import json
class ServerPingTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
class QueueAssertMixin(object):
def assertQueueLength(self, queue, count):
self._queue_events = queue.getEvents(0)
self.assertEqual(len(self._queue_events), count)
def tearDown(self):
self.user.delete()
self.server.delete()
class EventTest(unittest.TestCase):
def setUp(self):
events.EventQueue.flushAll()
self.server = Server.objects.create(hostname='localhost', secret='secret')
def testPing(self):
resp = self.client.get('/api/server/whoami',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token))
self.assertEqual(resp.status_code, 200)
resp = json.loads(resp.content)
self.assertEqual(resp["api-version"], 2)
self.assertTrue("api-version" in resp)
def tearDown(self):
events.EventQueue.flushAll()
self.server.delete()
if settings.CAMINUS_USE_BEANSTALKD:
class ServerEventTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
class ServerEventTest(EventTest, QueueAssertMixin):
def testHeartbeatCache(self):
oldBeat = cache.get('caminus-server-heartbeat-%s'%(self.server.id))
self.assertIsNone(oldBeat)
events.Dispatcher.dispatch(events.Event('server-heartbeat', port=self.server.port,
name=self.server.hostname, worldTimes={'world': 0}))
newBeat = cache.get('caminus-server-heartbeat-%s'%(self.server.id))
self.assertIsNotNone(oldBeat)
def tearDown(self):
self.user.delete()
self.server.delete()
class DispatcherTest(EventTest, QueueAssertMixin):
def testDispatch(self):
queue = events.ServerQueue(self.server)
self.assertQueueLength(queue, 0)
events.Dispatcher.dispatch(events.Event('test'))
self.assertQueueLength(queue, 1)
def testBroadcast(self):
events.server_broadcast("Test message")
response = json.loads(self.client.get('/api/server/events',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertTrue(len(response['events']) > 0)
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertEqual(response['result'], 'success')
def testBroadcast(self):
queue = events.ServerQueue(self.server)
self.assertQueueLength(queue, 0)
events.Dispatcher.broadcast('test')
self.assertQueueLength(queue, 1)
def testUserMessage(self):
events.user_message(self.user, "Test user message")
response = json.loads(self.client.get('/api/server/events',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertTrue(len(response['events']) > 0)
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertEqual(response['result'], 'success')
class WebQueueTest(unittest.TestCase):
def setUp(self):
events.EventQueue.flushAll()
class MOTDTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.server = Server.objects.create(hostname="localhost", secret="")
def tearDown(self):
events.EventQueue.flushAll()
def tearDown(self):
self.server.delete()
def testBroadcast(self):
queue = events.WebQueue(1)
events.WebQueue.broadcast(events.Event('test'))
webEvents = queue.getEvents()
self.assertEqual(len(webEvents), 1)
def testUnregisteredUser(self):
response = json.loads(self.client.get('/api/motd/NewUser').content)
self.assertIsInstance(response['motd'], list)
def testHugeBroadcast(self):
queues = []
for i in range(0, 30):
queues.append(events.WebQueue(i))
events.WebQueue.broadcast(events.Event('test'))
for queue in queues:
eventList = queue.getEvents()
self.assertEqual(len(eventList), 1)
def testRandomMOTD(self):
m = MOTD.objects.create(server=self.server, text="testMOTD")
q = Quote.objects.create(text="testQUOTE")
response = json.loads(self.client.get('/api/motd/NewUser').content)
m.delete()
q.delete()
foundMOTD = False
foundQuote = False
for line in response['motd']:
if line == "testMOTD":
foundMOTD = True
if line == '"testQUOTE"':
foundQuote = True
self.assertTrue(foundMOTD)
self.assertTrue(foundQuote)
def testActiveQueues(self):
events.WebQueue.clearActiveQueues()
self.assertEqual(len(events.WebQueue.activeQueues()), 0)
queues = []
for i in range(0, 30):
queues.append(events.WebQueue(i))
self.assertEqual(len(events.WebQueue.activeQueues()), len(queues))
class SessionTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
def testSingleUseEvent(self):
queue = events.WebQueue(1)
queue.sendEvent(events.Event('test'))
self.assertEqual(len(queue.getEvents(0)), 1)
self.assertEqual(len(queue.getEvents(0)), 0)
def tearDown(self):
self.user.delete()
self.server.delete()
class EventQueueTest(unittest.TestCase):
def setUp(self):
self.queue = events.EventQueue('test')
events.EventQueue.flushAll()
def testBannedStart(self):
b = Ban.objects.create(player=self.user.minecraftprofile, banner=self.user, reason="test")
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
b.delete()
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertFalse(session['success'])
def tearDown(self):
events.EventQueue.flushAll()
def testBadUserStart(self):
resp = self.client.post('/api/server/session/SomeUnknownUser/new', {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertFalse(session['success'])
def testSend(self):
self.queue.sendEvent(events.Event('test'))
def testInactiveStart(self):
self.user.is_active = False
self.user.save()
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.user.is_active = True
self.user.save()
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertFalse(session['success'])
def testReceive(self):
sentEvent = events.Event('test')
self.queue.sendEvent(sentEvent)
eventList = self.queue.getEvents()
self.assertEqual(len(eventList), 1)
self.assertEqual(type(eventList[0]), events.Event)
self.assertNotEqual(eventList[0]._id, sentEvent._id)
self.assertEqual(eventList[0]._type, sentEvent._type)
self.assertNotEqual(eventList[0]._conn, sentEvent._conn)
def testSessionStart(self):
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.assertEqual(resp.status_code, 200)
session = json.loads(resp.content)
self.assertTrue(session['success'])
def testFlush(self):
evt = events.Event('test')
for i in range(0, 300):
self.queue.sendEvent(evt)
self.assertEqual(len(self.queue.getEvents(0)), 300)
self.queue.flush()
self.assertEqual(len(self.queue.getEvents(0)), 0)
def testSessionEnd(self):
resp = self.client.post('/api/server/session/%s/new'%(self.user.minecraftprofile.mc_username), {'ip': '127.0.0.1'}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
session = json.loads(resp.content)
resp = self.client.get('/api/server/session/%s/close'%(self.user.minecraftprofile.mc_username), HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
self.assertEqual(resp.status_code, 200)
def testFlushAll(self):
evt = events.Event('test')
queues = []
for i in range(0, 10):
queues.append(events.EventQueue('test-%d'%(i)))
for i in range(0, 10):
for queue in queues:
queue.sendEvent(evt)
for queue in queues:
self.assertEqual(len(queue.getEvents(0)), 10)
events.EventQueue.flushAll()
for queue in queues:
self.assertEqual(len(queue.getEvents(0)), 0)
class PollTest(unittest.TestCase):
def setUp(self):
self.client = Client()
def testAnonymousPoll(self):
resp = self.client.get('/api/poll/0')
self.assertEqual(resp.status_code, 200)
data = json.loads(resp.content)
self.assertTrue('server-info' in data)
self.assertTrue('user-info' in data)
self.assertEqual(len(data['user-info']), 0)
class EventTest(unittest.TestCase):
def testEqual(self):
e = events.Event('test')
e2 = events.Event('test')
self.assertEqual(e, e2)
class EconomyTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.user.minecraftprofile.currencyaccount.balance=42
self.user.minecraftprofile.currencyaccount.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
def testNewEvent(self):
e = events.Event('test')
self.assertEqual(e._id, -1)
self.assertEqual(e._conn, None)
self.assertDictEqual(e._properties, {'data': None})
def tearDown(self):
self.user.delete()
self.server.delete()
def testValidTypes(self):
e = events.Event('quit', player="TestPlayer")
self.assertEqual(e.player, "TestPlayer")
def testBalanceQuery(self):
resp = self.client.get('/api/server/economy/ValidUsername', HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertEqual(data['balance'], 42)
def testInvalidType(self):
with self.assertRaises(ValueError):
e = events.Event('invalid')
def testDeposit(self):
resp = self.client.put('/api/server/economy/ValidUsername', {'delta': 100}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertEqual(data['balance'], 142)
self.assertTrue(data['success'])
def testMissingArgs(self):
with self.assertRaises(KeyError):
e = events.Event('quit')
def testWithdraw(self):
resp = self.client.put('/api/server/economy/ValidUsername', {'delta': -40}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertEqual(data['balance'], 2)
self.assertTrue(data['success'])
def testExtraArgs(self):
e = events.Event('quit', player='TestPlayer', extra='extra')
def testOverdraw(self):
resp = self.client.put('/api/server/economy/ValidUsername', {'delta': -400}, HTTP_AUTHORIZATION="X-Caminus %s"%(self.token))
data = json.loads(resp.content)
self.assertFalse(data['success'])
class EventMarshallTest(unittest.TestCase):
def testToDict(self):
self.assertDictEqual(events.Event('test')._toDict(), {
'_id': -1,
'_stamp': 0,
'_type': 'test',
'data': None,
})
def testJSONIdentity(self):
evt = events.Event('test')
self.assertEqual(events.Event._fromJSON(evt._toJSON()), evt)
def testDictIdentity(self):
evt = events.Event('test')
self.assertEqual(events.Event._fromDict(evt._toDict()), evt)
def testEmptyType(self):
desc = {}
data = events.Event._marshallType({}, desc, events.Event._datatypes(), 'marshall-test')
self.assertEqual(len(data), 0)
def testSimpleType(self):
desc = {'data': 'string'}
data = events.Event._marshallType({'data': 'value'}, desc,
events.Event._datatypes(), 'marshall-test')
self.assertEqual(len(data), 1)
self.assertTrue(isinstance(data['data'], str))
self.assertEqual(data['data'], 'value')
def testTypeMismatch(self):
desc = {'data': 'int'}
with self.assertRaises(ValueError):
events.Event._marshallType({'data': 'this is not an integer'}, desc,
events.Event._datatypes(), 'marshall-test')
def testComplexType(self):
desc = {'data': 'complex-type'}
typeDesc = {'complex-type': {'value': 'string'}}
data = events.Event._marshallType({'data': {'value': 'test'}}, desc, typeDesc, 'marshall-test')
self.assertDictEqual(data, {'data': {'value': 'test'}})
def testComplexTypeMismatch(self):
desc = {'data': 'complex-type'}
typeDesc = {'complex-type': {'value': 'string'}}
with self.assertRaises(KeyError):
events.Event._marshallType({'data': 'this is just a string'}, desc,
typeDesc, 'marshall-test')
def testMissingOptionalAttribute(self):
desc = {'data': events.OptionalEventProperty('string')}
data = events.Event._marshallType({}, desc, events.Event._datatypes(),
'marshall-test')
self.assertDictEqual(data, {'data': None})
def testOptionalAttribute(self):
desc = {'data': events.OptionalEventProperty('string')}
data = events.Event._marshallType({'data': 'value'}, desc,
events.Event._datatypes(),
'marshall-test')
self.assertDictEqual(data, {'data': 'value'})
def testMissingOptionalComplextType(self):
desc = {'data': events.OptionalEventProperty('complex-type')}
typeDesc = {'complex-type': {'value': 'string'}}
data = events.Event._marshallType({}, desc, typeDesc, 'marshall-test')
self.assertDictEqual(data, {'data': None})

View File

@@ -1,7 +1,7 @@
from django.db import models
from django.contrib.auth.models import User
import badges.api
from caminus.api.events import user_message
from caminus.api.events import Dispatcher
from notification import models as notification
from django.core.urlresolvers import reverse
@@ -58,7 +58,7 @@ class Award(models.Model):
super(Award, self).save(*args, **kwargs)
badges.api.badge_awarded.send_robust(sender=intern(str(self.badge.slug)), award=self)
notification.send([self.user], "badge_awarded", {"award": self, 'notice_description': self.badge, 'notice_url': reverse('user_profile')})
user_message(self.user, "You have been awarded the '%s' badge."%self.badge)
Dispatcher.user_message(self.user, "You have been awarded the '%s' badge."%self.badge)
def __unicode__(self):
return "%s for %s"%(self.badge.__unicode__(), self.user.__unicode__())

View File

@@ -18,3 +18,13 @@ class Bounty(models.Model):
self.save()
killer.currencyaccount.balance = F('balance') + self.price
killer.currencyaccount.save()
def handle_murder(sender, event, *args, **kwargs):
bounties = Bounty.objects.filter(target__mc_username=evt['payload']['player'])
killer = MinecraftProfile.objects.get(mc_username=evt['payload']['killer'])
for bounty in bounties:
bounty.close(killer)
if len(bounties) > 0:
Dispatcher.broadcast("The bounty on %s has been collected."%(evt['payload']['player']))
events.on_event.connect(handle_murder, sender=intern('player-murder'))

83
bulkops.py Normal file
View File

@@ -0,0 +1,83 @@
# Bulk insert/update DB operations for the Django ORM. Useful when
# inserting/updating lots of objects where the bottleneck is overhead
# in talking to the database. Instead of doing this
#
# for x in seq:
# o = SomeObject()
# o.foo = x
# o.save()
#
# or equivalently this
#
# for x in seq:
# SomeObject.objects.create(foo=x)
#
# do this
#
# l = []
# for x in seq:
# o = SomeObject()
# o.foo = x
# l.append(o)
# insert_many(l)
#
# Note that these operations are really simple. They won't work with
# many-to-many relationships, and you may have to divide really big
# lists into smaller chunks before sending them through.
#
# History
# 2010-12-10: quote column names, reported by Beres Botond.
def insert_many(objects, using="default"):
"""Insert list of Django objects in one SQL query. Objects must be
of the same Django model. Note that save is not called and signals
on the model are not raised."""
if not objects:
return
import django.db.models
from django.db import connections
con = connections[using]
model = objects[0].__class__
fields = [f for f in model._meta.fields if not isinstance(f, django.db.models.AutoField)]
parameters = []
for o in objects:
parameters.append(tuple(f.get_db_prep_save(f.pre_save(o, True), connection=con) for f in fields))
table = model._meta.db_table
column_names = ",".join(con.ops.quote_name(f.column) for f in fields)
placeholders = ",".join(("%s",) * len(fields))
con.cursor().executemany(
"insert into %s (%s) values (%s)" % (table, column_names, placeholders),
parameters)
def update_many(objects, fields=[], using="default"):
"""Update list of Django objects in one SQL query, optionally only
overwrite the given fields (as names, e.g. fields=["foo"]).
Objects must be of the same Django model. Note that save is not
called and signals on the model are not raised."""
if not objects:
return
import django.db.models
from django.db import connections
con = connections[using]
names = fields
meta = objects[0]._meta
fields = [f for f in meta.fields if not isinstance(f, django.db.models.AutoField) and (not names or f.name in names)]
if not fields:
raise ValueError("No fields to update, field names are %s." % names)
fields_with_pk = fields + [meta.pk]
parameters = []
for o in objects:
parameters.append(tuple(f.get_db_prep_save(f.pre_save(o, True), connection=con) for f in fields_with_pk))
table = meta.db_table
assignments = ",".join(("%s=%%s"% con.ops.quote_name(f.column)) for f in fields)
con.cursor().executemany(
"update %s set %s where %s=%%s" % (table, assignments, con.ops.quote_name(meta.pk.column)),
parameters)

View File

@@ -9,7 +9,7 @@ from django.template import RequestContext
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
from notification import models as notification
from api.events import server_broadcast, user_message
from api.events import Dispatcher
def index(request):
forums = models.Forum.objects.filter(parent=None)
@@ -45,7 +45,7 @@ def reply(request, topicID=None):
if reply.parent.user != request.user:
notification.send([reply.parent.user], "forum_reply", {"reply": reply, 'notice_url': reverse('forums.views.post', kwargs={'id':reply.id}), 'notice_description': reply.topic().title})
messages.info(request, "Reply successful")
user_message(reply.parent.user, "%s replied to your post in '%s'",
Dispatcher.user_message(reply.parent.user, "%s replied to your post in '%s'",
request.user, reply.topic().title)
return HttpResponseRedirect(reverse('forums.views.post', kwargs={"id":reply.id}))
return render_to_response('forums/reply.html', {"parent":parentPost, "form":form}, context_instance = RequestContext(request))
@@ -84,7 +84,7 @@ def newTopic(request, forumID=None):
topic.rootPost = reply
topic.save()
messages.info(request, "Posting successful")
server_broadcast("New forum topic: %s", topic.title)
Dispatcher.broadcast("New forum topic: %s", topic.title)
return HttpResponseRedirect(reverse('forums.views.post', kwargs={'id': reply.id}))
return render_to_response('forums/newTopic.html', {"forum":parentForum, "replyForm":replyForm, "topicForm": topicForm}, context_instance = RequestContext(request))

View File

@@ -12,7 +12,7 @@ from django.contrib.auth import authenticate, login
from django.core.exceptions import ObjectDoesNotExist
import forms
import models
from api.events import user_message
from api.events import Dispatcher
from forums.models import Forum
from minecraft.forms import ProfileForm
from minecraft.models import MinecraftProfile
@@ -80,7 +80,7 @@ def register(request):
profile.save()
user = authenticate(username=userForm.cleaned_data['username'], password=userForm.cleaned_data['password'])
notification.send_now([invite.creator], "invite_accepted", {"new_user": user})
user_message(invite.creator, "%s has accepted your invite."%(user.username))
Dispatcher.user_message(invite.creator, "%s has accepted your invite."%(user.username))
login(request, user)
del request.session['profile-invite']
return HttpResponseRedirect(reverse('welcome'))

View File

@@ -1,7 +1,7 @@
from django.core.management.base import NoArgsCommand
from market.models import MarketOrder
import json
from api.events import market_queue
from api.events import MarketQueue
from datetime import datetime
class Command(NoArgsCommand):
@@ -11,27 +11,27 @@ class Command(NoArgsCommand):
print "Processing currently open orders"
for order in MarketOrder.objects.filter(close_stamp__isnull=True):
order.process()
queue = market_queue()
queue = MarketQueue()
while True:
job = queue.reserve()
jobInfo = json.loads(job.body)
if jobInfo['event']['type'] == "market-order":
try:
order = MarketOrder.objects.get(id=jobInfo['event']['payload']['orderID'])
except MarketOrder.DoesNotExist:
# The orders might not be saved yet due to transactions
job.release()
continue
if order.close_stamp:
print "Got event for a closed order", order.id
job.delete()
continue
if order.quantity == 0:
print "Saving bugged order", order.id
order.close_stamp = datetime.now()
order.save()
job.delete()
continue
order.process()
for job in queue.getEvents():
jobInfo = json.loads(job.body)
if jobInfo['event']['type'] == "market-order":
try:
order = MarketOrder.objects.get(id=jobInfo['event']['payload']['orderID'])
except MarketOrder.DoesNotExist:
# The orders might not be saved yet due to transactions
job.release()
continue
if order.close_stamp:
print "Got event for a closed order", order.id
job.delete()
continue
if order.quantity == 0:
print "Saving bugged order", order.id
order.close_stamp = datetime.now()
order.save()
job.delete()
continue
order.process()
job.delete()
job.delete()

View File

@@ -4,7 +4,7 @@ from django.db import transaction
from django.db.models import F
from datetime import datetime
from django.db.models.signals import post_save
from api.events import queue_market_event, MarketOrderEvent
from api.events import MarketQueue, Event
from vault.models import VaultSlot
from django.contrib.auth.models import User
from minecraft.items import ITEMS
@@ -68,8 +68,9 @@ class MarketOrder(models.Model):
def enqueue_process(self):
if settings.CAMINUS_USE_BEANSTALKD:
evt = MarketOrderEvent(self.id)
queue_market_event(evt)
evt = Event('market-order', orderID=self.id)
queue = MarketQueue()
queue.sendEvent(evt)
else:
self.process()

View File

@@ -8,9 +8,3 @@ Replace this with more appropriate tests for your application.
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

View File

@@ -1,6 +1,9 @@
import models
def server_info(request):
period = models.Server.objects.all()[0].day_period()
try:
period = models.Server.objects.all()[0].day_period()
except IndexError, e:
period = "daytime"
return {'minecraft_servers': models.Server.objects.all(),
'first_day_period': period}

View File

@@ -15,6 +15,10 @@ class Item(models.Model):
@classmethod
def get(cls, material, damage=0, data=0):
if damage is None:
damage = 0
if data is None:
data = 0
return cls.objects.get_or_create(material=material, damage=damage,
data=data)[0]
@@ -174,5 +178,6 @@ import api.events
def kick_banned_user(sender, instance, created, **kwargs):
player = instance.player
if player.isBanned():
api.events.broadcast_server_event(api.events.KickEvent(player.mc_username, instance.reason))
api.events.ServerQueue.broadcast(api.events.Event('player-kick',
player=player.mc_username, message=instance.reason))
post_save.connect(kick_banned_user, sender=Ban)

View File

@@ -8,7 +8,7 @@ from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from notification import models as notification
from django.contrib import messages
from api.events import user_message
from api.events import Dispatcher
@login_required
def create(request):
@@ -24,7 +24,7 @@ def create(request):
adminUsers = User.objects.filter(is_staff=True)
notification.send(adminUsers, "petition_opened", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id})
for user in adminUsers:
user_message(user, "%s has opened a petition."%(request.user))
Dispatcher.user_message(user, "%s has opened a petition."%(request.user))
messages.info(request, "Petition created.")
return HttpResponseRedirect(reverse('petition.views.view', kwargs={"id":petition.id}))
return render_to_response('petition/create.html', {'form':form}, context_instance = RequestContext(request))
@@ -59,7 +59,7 @@ def comment(request, id):
comment.save()
adminUsers = User.objects.filter(is_staff=True)
for user in adminUsers:
user_message(user, "%s has opened a petition."%(request.user))
Dispatcher.user_message(user, "%s has opened a petition."%(request.user))
notification.send(adminUsers, "petition_commented", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id, 'comment': comment})
if comment.author != petition.author:
notification.send([petition.author], "petition_commented", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id, 'comment': comment})

View File

@@ -11,3 +11,4 @@ pydot==1.0.28
beanstalkc
stripe
django-devserver
PyYAML==3.10

View File

@@ -4,9 +4,10 @@ if [ ! -f virtualenv/bin/activate ];then
fi
source virtualenv/bin/activate
pip install -r pip-requirements
coverage erase
coverage run ./manage.py test $@
ret=$?
if [ $ret -eq 0 ];then
coverage report -m --include=\* --omit=\*/migrations/\*,settings.py,local_settings.py,manage.py
coverage report -m --include=\* --omit=\*/migrations/\*,settings.py,local_settings.py,manage.py,bulkops.py
fi
exit $ret

View File

@@ -104,7 +104,6 @@ MIDDLEWARE_CLASSES = (
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'django.middleware.gzip.GZipMiddleware',
'caminus.devtools.PrintExceptionMiddleware',
)
ROOT_URLCONF = 'caminus.urls'

View File

@@ -38,6 +38,10 @@ div[class|="inventory-item"] .quantity {
padding: 3px;
}
div[class|="inventory-item"] .damage.empty{
display: none;
}
div[class|="inventory-item"] .damage {
position: absolute;
bottom: 0;

View File

@@ -20,7 +20,6 @@ EventPoller.prototype.start = function() {
};
EventPoller.prototype.dispatchEvent = function(evt) {
console.log(evt['type']);
if (evt['type'] in this.handlers) {
this.handlers[evt['type']].forEach(function (callback, idx, handlers) {
callback(evt, evt['payload']);
@@ -50,6 +49,7 @@ EventPoller.prototype._failedPoll = function(data) {
});
}
this.id = 0;
window.setTimeout(this.poll.bind(this), 5000);
}
EventPoller.prototype._successfulPoll = function(data) {
@@ -78,6 +78,7 @@ EventPoller.prototype._successfulPoll = function(data) {
var that = this;
$(data['events']).each(function(idx, evt) {
console.log(evt['type']);
that.dispatchEvent(evt);
});

78
static/js/vault.js Normal file
View File

@@ -0,0 +1,78 @@
var InventoryWidget = function(tableElement) {
this.e = $(tableElement);
this.slots = new Array();
this.e.find("td[data-position]").each((function (idx, td) {
var slot = new ItemSlot(td);
this.slots[slot.getPosition()] = slot;
}).bind(this));
Caminus.eventPoll.onEvent('vault-contents', this._vaultEvent.bind(this));
}
InventoryWidget.prototype._vaultEvent = function(evt, payload) {
$(payload.items).each((function(idx, item) {
var slot = this.slotByPosition(item.position);
slot.updateFromJSON(item);
}).bind(this));
};
InventoryWidget.prototype.slotByPosition = function(pos) {
console.log(this);
return this.slots[pos];
}
var ItemSlot = function(tdElement) {
this.e = $(tdElement);
this.item = $(this.e.find('div[class|="inventory-item"]')[0]);
}
ItemSlot.prototype.getPosition = function() {
return this.e.data('position');
}
ItemSlot.prototype.updateFromJSON = function(data) {
this.setMaterial(data.item);
this.setDurability(data.durablility);
this.setDamage(data.damage);
this.setQuantity(data.quantity);
this.setName(data.name);
}
ItemSlot.prototype.setName = function(s) {
this.item.data('name', s);
this.item.find('.name').html(s);
}
ItemSlot.prototype.setMaterial = function(id) {
this.item.data('material', id);
this.item.attr('class',"inventory-item-"+id);
}
ItemSlot.prototype.setQuantity = function(qty) {
this.item.data('quantity', qty);
this.item.find('.quantity').html(qty);
}
ItemSlot.prototype.setDamage = function(dmg) {
this.item.data('damage', dmg);
this.item.find('.damage', "50%");
if (dmg == 0)
this.item.find('.damage').addClass("empty");
else
this.item.find('.damage').removeClass("empty");
}
ItemSlot.prototype.getDamage = function() {
return this.item.data('damage');
}
ItemSlot.prototype.setDurability = function(d) {
this.item.data('durability', d);
this.setDamage(this.getDamage());
}
$(document).ready(function() {
$('table.inventory').each(function (idx, inventory) {
new InventoryWidget(inventory);
});
});

View File

@@ -1,12 +1,12 @@
<div class="inventory-item-{{item.material}}">
{% if quantity > 0 %}
<div class="quantity">{{quantity}}</div>
<div class="inventory-item-{{item.material}}" data-material="{{item.material}}"
data-quantity="{{quantity}}" data-damage="{{item.damage}}">
<div class="quantity">
{% if quantity > 0 %}{{quantity}}
</div>
{% endif %}
<div class="name">{{item.name}}</div>
{% if item.damage %}
<div class="damage">
<div class="current"
style="width:{{item.damagePct}}%"></div>
</div>
{% endif %}
<div class="damage {% if not item.damage %}empty{%endif%}">
<div class="current"
style="width:{{item.damagePct}}%"></div>
</div>
</div>

View File

@@ -1,8 +1,14 @@
{% extends "base_simple.html" %}
{%load static %}
{% get_static_prefix as STATIC_PREFIX %}
{% block title %}Vault{% endblock %}
{% block sectiontitle %}Your Vault{% endblock %}
{% block extrahead %}
<script type="text/javascript" language="javascript" src="{{STATIC_PREFIX}}/js/vault.js"></script>
{% endblock %}
{% block content %}
<table class="inventory">
<tr>
@@ -11,10 +17,8 @@
</tr>
<tr>
{% endif %}
<td>
{% if slot.item %}
<td data-id="{{slot.id}}" data-position="{{slot.position}}">
{% include 'common/item.html' with quantity=slot.quantity item=slot.item %}
{% endif %}
</td>
{% endfor %}
</tr>

View File

@@ -1,9 +1,10 @@
from django.db import models
from minecraft.models import MinecraftProfile
from django.db.models.signals import post_save, post_delete
from api.events import VaultContentsEvent, broadcast_server_event
from api.events import Event, ServerQueue, WebQueue
from minecraft.models import Item
from django.conf import settings
from caminus.bulkops import insert_many
class VaultError(Exception):
pass
@@ -59,43 +60,46 @@ class VaultSlot(models.Model):
raise VaultError, "Insufficient available slots."
return slots
def send_vault_delete(sender, instance, *args, **kwargs):
slots = [
{
'item': None,
'quantity': -1,
'damage': None,
'data': None,
'position': instance.position
}
]
broadcast_server_event(VaultContentsEvent(instance.player.mc_username,
slots))
def send_vault_update(sender, instance, created, *args, **kwargs):
if created and instance.item:
if instance.item:
slots = [
{
'item': instance.item.material,
'material': instance.item.material,
'quantity': instance.quantity,
'damage': instance.item.damage,
'data': instance.item.data,
'position': instance.position
'position': instance.position,
'name': instance.item.name(),
'durability': instance.item.metadata['durability']
}
]
broadcast_server_event(VaultContentsEvent(instance.player.mc_username, slots))
else:
slots = [
{
'material': None,
'quantity': None,
'damage': None,
'data': None,
'position': instance.position,
'name': None,
'durability': None
}
]
evt = Event('vault-contents', player=instance.player.mc_username, items=slots)
ServerQueue.broadcast(evt)
WebQueue.broadcast(evt)
post_save.connect(send_vault_update, sender=VaultSlot, dispatch_uid='derp')
post_delete.connect(send_vault_delete, sender=VaultSlot, dispatch_uid='derp')
def create_initial_slots(sender, instance, created, *args, **kwargs):
def ensure_initial_slots(sender, instance, created, *args, **kwargs):
slots = instance.vault_slots.all()
if len(slots) < settings.CAMINUS_VAULT_SLOTS:
neededPositions = range(0, settings.CAMINUS_VAULT_SLOTS)
for s in slots:
neededPositions.remove(s.position)
bulk = []
for pos in neededPositions:
VaultSlot.objects.create(player=instance, position=pos)
post_save.connect(create_initial_slots, sender=MinecraftProfile)
bulk.append(VaultSlot(player=instance, position=pos))
insert_many(bulk)
post_save.connect(ensure_initial_slots, sender=MinecraftProfile)