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):
def __init__(self, *args, **kwargs):
super().__init__(self)
super().__init__()
self.activity = kwargs.copy()
self.activity['type'] = 'Tombstone'
@ -67,4 +67,13 @@ class TombstoneException(Exception):
def __str__(self):
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_kepi import object_type_registry
from django_kepi import object_type_registry, resolve
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from random import randint
import random
import json
import datetime
import warnings
@ -35,7 +35,7 @@ class Cobject(models.Model):
result = ''
for i in range(6):
digit = randint(0, 35)
digit = random.randint(0, 35)
# yes, I know this can be done more efficiently.
# I want it to be readable.
@ -62,10 +62,7 @@ class Cobject(models.Model):
if self.remote_id is not None:
return self.remote_id
else:
return settings.KEPI['URL_FORMAT'] % {
'type': self.__class__.__name__.lower(),
'slug': self.slug,
}
return settings.KEPI['ACTIVITY_URL_FORMAT'] % (self.slug, )
def is_local(self):
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):
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(
max_length=255,
primary_key=True,
)
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
default=new_activity_identifier,
)
actor = models.URLField(
@ -403,16 +420,154 @@ class Activity(models.Model):
blank=True,
)
fobject_type = models.CharField(
max_length=255,
blank=True,
)
fobject = models.URLField(
max_length=255,
blank=True,
)
valid = models.BooleanField(
target = models.URLField(
max_length=255,
blank=True,
)
active = models.BooleanField(
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):
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
KEPI = {
'URL_FORMAT': 'https://example.com/activities/%(type)s/%(slug)s',
'ACTIVITY_URL_FORMAT': 'https://example.com/activities/%s',
}
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())