mirror of
https://github.com/jazzband/django-ddp.git
synced 2026-03-17 06:50:24 +00:00
387 lines
12 KiB
Python
387 lines
12 KiB
Python
"""Django DDP models."""
|
|
from __future__ import absolute_import
|
|
|
|
import collections
|
|
import os
|
|
|
|
from django.db import models, transaction
|
|
from django.db.models.fields import NOT_PROVIDED
|
|
from django.conf import settings
|
|
from django.contrib.contenttypes.fields import (
|
|
GenericRelation, GenericForeignKey,
|
|
)
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.utils.encoding import python_2_unicode_compatible
|
|
import ejson
|
|
from dddp import meteor_random_id
|
|
|
|
|
|
def get_meteor_id(obj_or_model, obj_pk=None):
|
|
"""Return an Alea ID for the given object."""
|
|
if obj_or_model is None:
|
|
return None
|
|
# Django model._meta is now public API -> pylint: disable=W0212
|
|
meta = obj_or_model._meta
|
|
model = meta.model
|
|
if model is ObjectMapping:
|
|
# this doesn't make sense - raise TypeError
|
|
raise TypeError("Can't map ObjectMapping instances through self.")
|
|
|
|
# try getting value of AleaIdField straight from instance if possible
|
|
if isinstance(obj_or_model, model):
|
|
# obj_or_model is an instance, not a model.
|
|
if isinstance(meta.pk, AleaIdField):
|
|
return obj_or_model.pk
|
|
if obj_pk is None:
|
|
# fall back to primary key, but coerce as string type for lookup.
|
|
obj_pk = str(obj_or_model.pk)
|
|
alea_unique_fields = [
|
|
field
|
|
for field in meta.local_fields
|
|
if isinstance(field, AleaIdField) and field.unique
|
|
]
|
|
if len(alea_unique_fields) == 1:
|
|
# found an AleaIdField with unique=True, assume it's got the value.
|
|
aid = alea_unique_fields[0].attname
|
|
if isinstance(obj_or_model, model):
|
|
val = getattr(obj_or_model, aid)
|
|
elif obj_pk is None:
|
|
val = None
|
|
else:
|
|
val = model.objects.values_list(aid, flat=True).get(
|
|
pk=obj_pk,
|
|
)
|
|
if val:
|
|
return val
|
|
|
|
if obj_pk is None:
|
|
# bail out if args are (model, pk) but pk is None.
|
|
return None
|
|
|
|
# fallback to using AleaIdField from ObjectMapping model.
|
|
content_type = ContentType.objects.get_for_model(model)
|
|
try:
|
|
return ObjectMapping.objects.values_list(
|
|
'meteor_id', flat=True,
|
|
).get(
|
|
content_type=content_type,
|
|
object_id=obj_pk,
|
|
)
|
|
except ObjectDoesNotExist:
|
|
return ObjectMapping.objects.create(
|
|
content_type=content_type,
|
|
object_id=obj_pk,
|
|
meteor_id=meteor_random_id('/collection/%s' % meta),
|
|
).meteor_id
|
|
get_meteor_id.short_description = 'DDP ID' # nice title for admin list_display
|
|
|
|
|
|
def get_meteor_ids(model, object_ids):
|
|
"""Return Alea ID mapping for all given ids of specified model."""
|
|
# Django model._meta is now public API -> pylint: disable=W0212
|
|
meta = model._meta
|
|
result = collections.OrderedDict(
|
|
(str(obj_pk), None)
|
|
for obj_pk
|
|
in object_ids
|
|
)
|
|
if isinstance(meta.pk, AleaIdField):
|
|
# primary_key is an AleaIdField, use it.
|
|
return collections.OrderedDict(
|
|
(obj_pk, obj_pk) for obj_pk in object_ids
|
|
)
|
|
alea_unique_fields = [
|
|
field
|
|
for field in meta.local_fields
|
|
if isinstance(field, AleaIdField) and field.unique and not field.null
|
|
]
|
|
if len(alea_unique_fields) == 1:
|
|
aid = alea_unique_fields[0].name
|
|
query = model.objects.filter(
|
|
pk__in=object_ids,
|
|
).values_list('pk', aid)
|
|
else:
|
|
content_type = ContentType.objects.get_for_model(model)
|
|
query = ObjectMapping.objects.filter(
|
|
content_type=content_type,
|
|
object_id__in=list(result)
|
|
).values_list('object_id', 'meteor_id')
|
|
for obj_pk, meteor_id in query:
|
|
result[str(obj_pk)] = meteor_id
|
|
for obj_pk, meteor_id in result.items():
|
|
if meteor_id is None:
|
|
result[obj_pk] = get_meteor_id(model, obj_pk)
|
|
return result
|
|
|
|
|
|
def get_object_id(model, meteor_id):
|
|
"""Return an object ID for the given meteor_id."""
|
|
if meteor_id is None:
|
|
return None
|
|
# Django model._meta is now public API -> pylint: disable=W0212
|
|
meta = model._meta
|
|
|
|
if model is ObjectMapping:
|
|
# this doesn't make sense - raise TypeError
|
|
raise TypeError("Can't map ObjectMapping instances through self.")
|
|
|
|
if isinstance(meta.pk, AleaIdField):
|
|
# meteor_id is the primary key
|
|
return meteor_id
|
|
|
|
alea_unique_fields = [
|
|
field
|
|
for field in meta.local_fields
|
|
if isinstance(field, AleaIdField) and field.unique
|
|
]
|
|
if len(alea_unique_fields) == 1:
|
|
# found an AleaIdField with unique=True, assume it's got the value.
|
|
val = model.objects.values_list(
|
|
'pk', flat=True,
|
|
).get(**{
|
|
alea_unique_fields[0].attname: meteor_id,
|
|
})
|
|
if val:
|
|
return val
|
|
|
|
content_type = ContentType.objects.get_for_model(model)
|
|
return ObjectMapping.objects.filter(
|
|
content_type=content_type,
|
|
meteor_id=meteor_id,
|
|
).values_list('object_id', flat=True).get()
|
|
|
|
|
|
def get_object_ids(model, meteor_ids):
|
|
"""Return all object IDs for the given meteor_ids."""
|
|
if model is ObjectMapping:
|
|
# this doesn't make sense - raise TypeError
|
|
raise TypeError("Can't map ObjectMapping instances through self.")
|
|
# Django model._meta is now public API -> pylint: disable=W0212
|
|
meta = model._meta
|
|
alea_unique_fields = [
|
|
field
|
|
for field in meta.local_fields
|
|
if isinstance(field, AleaIdField) and field.unique and not field.null
|
|
]
|
|
result = collections.OrderedDict(
|
|
(str(meteor_id), None)
|
|
for meteor_id
|
|
in meteor_ids
|
|
)
|
|
if len(alea_unique_fields) == 1:
|
|
aid = alea_unique_fields[0].name
|
|
query = model.objects.filter(**{
|
|
'%s__in' % aid: meteor_ids,
|
|
}).values_list(aid, 'pk')
|
|
else:
|
|
content_type = ContentType.objects.get_for_model(model)
|
|
query = ObjectMapping.objects.filter(
|
|
content_type=content_type,
|
|
meteor_id__in=meteor_ids,
|
|
).values_list('meteor_id', 'object_id')
|
|
for meteor_id, object_id in query:
|
|
result[meteor_id] = object_id
|
|
return result
|
|
|
|
|
|
def get_object(model, meteor_id, *args, **kwargs):
|
|
"""Return an object for the given meteor_id."""
|
|
# Django model._meta is now public API -> pylint: disable=W0212
|
|
meta = model._meta
|
|
if isinstance(meta.pk, AleaIdField):
|
|
# meteor_id is the primary key
|
|
return model.objects.filter(*args, **kwargs).get(pk=meteor_id)
|
|
|
|
alea_unique_fields = [
|
|
field
|
|
for field in meta.local_fields
|
|
if isinstance(field, AleaIdField) and field.unique and not field.null
|
|
]
|
|
if len(alea_unique_fields) == 1:
|
|
return model.objects.filter(*args, **kwargs).get(**{
|
|
alea_unique_fields[0].name: meteor_id,
|
|
})
|
|
|
|
return model.objects.filter(*args, **kwargs).get(
|
|
pk=get_object_id(model, meteor_id),
|
|
)
|
|
|
|
|
|
class AleaIdField(models.CharField):
|
|
|
|
"""CharField that generates its own values using Alea PRNG before INSERT."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Assume max_length of 17 to match Meteor implementation."""
|
|
kwargs['blank'] = True
|
|
kwargs.setdefault('verbose_name', 'Alea ID')
|
|
kwargs.setdefault('max_length', 17)
|
|
super(AleaIdField, self).__init__(*args, **kwargs)
|
|
|
|
def deconstruct(self):
|
|
"""Return arguments to pass to __init__() to re-create this field."""
|
|
name, path, args, kwargs = super(AleaIdField, self).deconstruct()
|
|
del kwargs['blank']
|
|
return name, path, args, kwargs
|
|
|
|
def get_seeded_value(self, instance):
|
|
"""Generate a syncronised value."""
|
|
# Django model._meta is public API -> pylint: disable=W0212
|
|
return meteor_random_id(
|
|
'/collection/%s' % instance._meta, self.max_length,
|
|
)
|
|
|
|
def get_pk_value_on_save(self, instance):
|
|
"""Generate ID if required."""
|
|
value = super(AleaIdField, self).get_pk_value_on_save(instance)
|
|
if not value:
|
|
value = self.get_seeded_value(instance)
|
|
return value
|
|
|
|
def pre_save(self, model_instance, add):
|
|
"""Generate ID if required."""
|
|
value = super(AleaIdField, self).pre_save(model_instance, add)
|
|
if (not value) and self.default in (meteor_random_id, NOT_PROVIDED):
|
|
value = self.get_seeded_value(model_instance)
|
|
setattr(model_instance, self.attname, value)
|
|
return value
|
|
|
|
|
|
# Please don't hate me...
|
|
AID_KWARGS = {}
|
|
|
|
if os.environ.get('AID_MIGRATE_STEP', '') == '1':
|
|
AID_KWARGS['null'] = True # ...please?
|
|
|
|
|
|
class AleaIdMixin(models.Model):
|
|
|
|
"""Django model mixin that provides AleaIdField field (as aid)."""
|
|
|
|
aid = AleaIdField(unique=True, editable=True, **AID_KWARGS)
|
|
|
|
class Meta(object):
|
|
|
|
"""Model meta options for AleaIdMixin."""
|
|
|
|
abstract = True
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class ObjectMapping(models.Model):
|
|
|
|
"""Mapping from regular Django model primary keys to Meteor object IDs."""
|
|
|
|
meteor_id = AleaIdField()
|
|
content_type = models.ForeignKey(ContentType, db_index=True)
|
|
object_id = models.CharField(max_length=255)
|
|
content_object = GenericForeignKey('content_type', 'object_id')
|
|
|
|
def __str__(self):
|
|
"""Text representation of a mapping."""
|
|
return '%s: %s[%s]' % (
|
|
self.meteor_id, self.content_type, self.object_id,
|
|
)
|
|
|
|
class Meta(object):
|
|
|
|
"""Meta info for ObjectMapping model."""
|
|
|
|
unique_together = [
|
|
['content_type', 'meteor_id'],
|
|
]
|
|
index_together = [
|
|
['content_type', 'object_id'],
|
|
['content_type', 'meteor_id'],
|
|
]
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class Connection(models.Model, object):
|
|
|
|
"""Django DDP connection instance."""
|
|
|
|
connection_id = AleaIdField()
|
|
server_addr = models.CharField(max_length=255)
|
|
remote_addr = models.CharField(max_length=255)
|
|
version = models.CharField(max_length=255)
|
|
|
|
def __str__(self):
|
|
"""Text representation of subscription."""
|
|
return u'%s/\u200b%s' % (
|
|
self.connection_id,
|
|
self.remote_addr,
|
|
)
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class Subscription(models.Model, object):
|
|
|
|
"""Subscription to a publication with params."""
|
|
|
|
_publication_cache = {}
|
|
connection = models.ForeignKey(Connection)
|
|
sub_id = models.CharField(max_length=17)
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True)
|
|
publication = models.CharField(max_length=255)
|
|
params_ejson = models.TextField(default='{}')
|
|
|
|
class Meta(object):
|
|
|
|
"""Subscription model meta."""
|
|
|
|
unique_together = [
|
|
['connection', 'sub_id'],
|
|
]
|
|
|
|
def __str__(self):
|
|
"""Text representation of subscription."""
|
|
return u'%s/\u200b%s/\u200b%s: %s%s' % (
|
|
self.user,
|
|
self.connection_id,
|
|
self.sub_id,
|
|
self.publication,
|
|
self.params_ejson,
|
|
)
|
|
|
|
def get_params(self):
|
|
"""Get params dict."""
|
|
return ejson.loads(self.params_ejson or '{}')
|
|
|
|
def set_params(self, vals):
|
|
"""Set params dict."""
|
|
self.params_ejson = ejson.dumps(vals or {})
|
|
|
|
params = property(get_params, set_params)
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class SubscriptionCollection(models.Model):
|
|
|
|
"""Collections for a subscription."""
|
|
|
|
subscription = models.ForeignKey(Subscription, related_name='collections')
|
|
model_name = models.CharField(max_length=255)
|
|
collection_name = models.CharField(max_length=255)
|
|
|
|
def __str__(self):
|
|
"""Human readable representation of colleciton for a subscription."""
|
|
return u'%s \u200b %s (%s)' % (
|
|
self.subscription,
|
|
self.collection_name,
|
|
self.model_name,
|
|
)
|
|
|
|
|
|
class ObjectMappingMixin(models.Model):
|
|
|
|
"""Model mixin that provides GenericRelation back to ObjectMapping model."""
|
|
|
|
object_mapping = GenericRelation(ObjectMapping)
|
|
|
|
class Meta(object):
|
|
|
|
"""ObjectMappingMixin model meta options."""
|
|
|
|
abstract = True
|