New design for Activity, since we need to deserialise everything in the same place.

NeedToFetchException added.

rm mistaken params in TombstoneException's call to superclass's constructor.

URL_FORMAT -> ACTIVITY_URL_FORMAT for clarity.

rm tests for old code which is going away soon.
thingy_objects
Marnanel Thurman 2018-09-10 13:56:31 +01:00
rodzic bd907af51b
commit 8c3d13ed46
4 zmienionych plików z 190 dodań i 177 usunięć

Wyświetl plik

@ -59,7 +59,7 @@ def resolve(identifier, atype=None):
class TombstoneException(Exception): class TombstoneException(Exception):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(self) super().__init__()
self.activity = kwargs.copy() self.activity = kwargs.copy()
self.activity['type'] = 'Tombstone' self.activity['type'] = 'Tombstone'
@ -67,4 +67,13 @@ class TombstoneException(Exception):
def __str__(self): def __str__(self):
return str(self.activity) return str(self.activity)
class NeedToFetchException(Exception):
def __init__(self, *args, **kwargs):
super().__init__()
self.url = kwargs['url']
def __str__(self):
return self.url

Wyświetl plik

@ -1,9 +1,9 @@
from django.db import models from django.db import models
from django_kepi import object_type_registry from django_kepi import object_type_registry, resolve
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.conf import settings from django.conf import settings
from random import randint import random
import json import json
import datetime import datetime
import warnings import warnings
@ -35,7 +35,7 @@ class Cobject(models.Model):
result = '' result = ''
for i in range(6): for i in range(6):
digit = randint(0, 35) digit = random.randint(0, 35)
# yes, I know this can be done more efficiently. # yes, I know this can be done more efficiently.
# I want it to be readable. # I want it to be readable.
@ -62,10 +62,7 @@ class Cobject(models.Model):
if self.remote_id is not None: if self.remote_id is not None:
return self.remote_id return self.remote_id
else: else:
return settings.KEPI['URL_FORMAT'] % { return settings.KEPI['ACTIVITY_URL_FORMAT'] % (self.slug, )
'type': self.__class__.__name__.lower(),
'slug': self.slug,
}
def is_local(self): def is_local(self):
return self.remote_id is None return self.remote_id is None
@ -376,26 +373,46 @@ class RequestingAccess(UserRelationship):
####################### #######################
def new_activity_identifier():
template = settings.KEPI['ACTIVITY_URL_FORMAT']
slug = '%08x' % (random.randint(0, 0xffffffff),)
return template % (slug,)
class Activity(models.Model): class Activity(models.Model):
CREATE='C'
UPDATE='U'
DELETE='D'
FOLLOW='F'
ADD='+'
REMOVE='-'
LIKE='L'
UNDO='U'
ACCEPT='A'
REJECT='R'
ACTIVITY_TYPE_CHOICES = (
(CREATE, 'Create'),
(UPDATE, 'Update'),
(DELETE, 'Delete'),
(FOLLOW, 'Follow'),
(ADD, 'Add'),
(REMOVE, 'Remove'),
(LIKE, 'Like'),
(UNDO, 'Undo'),
(ACCEPT, 'Accept'),
(REJECT, 'Reject'),
)
atype = models.URLField(
max_length=1,
choices=ACTIVITY_TYPE_CHOICES,
)
identifier = models.URLField( identifier = models.URLField(
max_length=255, max_length=255,
primary_key=True, primary_key=True,
) default=new_activity_identifier,
atype = models.CharField(
max_length=1,
# FIXME: an enum of
# C=create
# U=update
# D=delete
# F=follow
# +=add
# -=remove
# L=like
# U=undo
# A=accept
# R=reject
) )
actor = models.URLField( actor = models.URLField(
@ -403,16 +420,154 @@ class Activity(models.Model):
blank=True, blank=True,
) )
fobject_type = models.CharField(
max_length=255,
blank=True,
)
fobject = models.URLField( fobject = models.URLField(
max_length=255, max_length=255,
blank=True, blank=True,
) )
valid = models.BooleanField( target = models.URLField(
max_length=255,
blank=True,
)
active = models.BooleanField(
default=True, default=True,
) )
# XXX Updates from clients are partial,
# but updates from remote sites are total.
# We don't currently let clients create Activities,
# but if we ever do, we should flag which it was.
def __str__(self): def __str__(self):
pass # XXX FIXME
if self.active:
inactive_warning = ''
else:
inactive_warning = ' INACTIVE'
result = '[%s %s%s]' % (
self.atype,
self.identifier,
inactive_warning,
)
return result
@property
def activity(self):
result = {
'id': self.identifier,
'type': self.get_atype_display(),
}
for optional in ['actor', 'object', 'published', 'updated', 'target']:
if optional=='object':
fieldname='fobject'
else:
fieldname=optional
value = getattr(self, fieldname)
if value is not None:
result[optional] = value
# XXX should we mark "inactive" somehow?
return result
TYPES = {
# actor object target
'Create': (True, True, False),
'Update': (True, True, False),
'Delete': (True, True, False),
'Follow': (True, True, False),
'Add': (True, False, True),
'Remove': (True, False, True),
'Like': (True, True, False),
'Undo': (False, True, False),
'Accept': (False, True, False),
'Reject': (False, True, False),
}
@classmethod
def deserialize(cls, value,
local=False):
if 'type' not in value:
raise ValueError("Activities must have a type")
if 'id' not in value and not local:
raise ValueError("Remote activities must have an id")
fields = {
'identifier': value.get('id', None),
'type': value['type'],
'active': True,
}
try:
need_actor, need_object, need_target = TYPES[value['type']]
except KeyError:
raise ValueError('{} is not an Activity type'.format(value['type']))
if need_actor!=('actor' in value) or \
need_object!=('object' in value) or \
need_target!=('target' in value):
raise ValueError('Wrong parameters for type')
# TODO: Sometimes an incoming Activity is trustworthy in
# telling us about a remote object. At present, for
# simplicity, we don't trust anybody. If we don't have
# the object in the cache, we must fetch it.
# In each case, the field is either specified as
# a Link or as an Object. If it's a Link, it will
# consist of a single string, which is our URL.
# If it's an Object, it will be a dict whose 'id'
# field is our URL.
for atype in ('actor', 'object', 'target'):
if atype not in value:
# if it's not there, it's not supposed to be there:
# we checked for that earlier.
continue
if isinstance(value[atype], str):
check_url = value[atype]
check_type = None # check everything
else:
try:
check_url = value[atype]['id']
except KeyError:
raise ValueError('Explicit objects must have an id')
try:
check_type = value[atype]['type']
except KeyError:
check_type = None # check everything
referent = resolve(
identifier=check_url,
atype=check_type,
)
if referent is None:
# we don't know about it,
# but we need to.
raise NeedToFetchException(check_url)
# okay, we can let them use it
fields[atype] = check_url
result = cls(**fields)
return result
# TODO: there should be a clean() method with the same
# checks as deserialize().

Wyświetl plik

@ -1,7 +1,7 @@
from django.urls import path, include from django.urls import path, include
KEPI = { KEPI = {
'URL_FORMAT': 'https://example.com/activities/%(type)s/%(slug)s', 'ACTIVITY_URL_FORMAT': 'https://example.com/activities/%s',
} }
INSTALLED_APPS = ( INSTALLED_APPS = (

Wyświetl plik

@ -1,151 +0,0 @@
from django.test import TestCase, Client
from django_kepi.models import Create, Like, Update, Delete, lookup
from things_for_testing.models import ThingUser, ThingArticle
import datetime
class UserTests(TestCase):
def test_create(self):
actor = ThingUser(
name='Dijkstra',
)
actor.save()
article = ThingArticle(
title='Go To statement considered harmful',
)
article.save()
create = Create(
actor=actor,
fobject=article,
)
create.save()
serialized = create.serialize()
for field in [
'id', 'type',
'object', 'actor',
'published', 'updated',
]:
self.assertIn(field, serialized)
self.assertIsInstance(
serialized['id'],
str)
self.assertEqual(
serialized['type'],
'Create')
self.assertDictEqual(
serialized['object'],
article.serialize(),
)
self.assertEqual(
serialized['actor'],
'https://example.com/user/Dijkstra')
self.assertIsInstance(
serialized['published'],
datetime.datetime,
)
self.assertIsInstance(
serialized['updated'],
datetime.datetime,
)
looked_up = lookup('create', create.slug)
self.assertEqual(
looked_up,
create,
)
def test_update(self):
actor = ThingUser(
name='Dijkstra',
)
actor.save()
article = ThingArticle(
title='Go To statement considered harmful',
)
article.save()
create = Create(
actor=actor,
fobject=article,
)
create.save()
article2 = ThingArticle(
title='Actually I rather like spaghetti code',
)
article2.save()
update = Update(
actor=actor,
fobject=article2,
)
update.save()
def test_delete(self):
actor = ThingUser(
name='Dijkstra',
)
actor.save()
article = ThingArticle(
title='Go To statement considered harmful',
)
article.save()
create = Create(
actor=actor,
fobject=article,
)
create.save()
delete = Delete(
actor=actor,
fobject=article,
)
delete.save()
# fetch by object ID (we can't do this atm) will get Tombstone
#raise ValueError(str(activity.serialize()))
def test_like(self):
liker = ThingUser(
name='Uncle Bulgaria',
)
liker.save()
author = ThingUser(
name='Dijkstra',
)
author.save()
article = ThingArticle(
title='Go To statement considered harmful',
)
article.save()
create = Create(
actor=author,
fobject=article,
)
create.save()
like = Like(
actor=liker,
fobject=article,
)
like.save()
#raise ValueError(like.serialize_as_str())