kopia lustrzana https://gitlab.com/marnanel/chapeau
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
rodzic
bd907af51b
commit
8c3d13ed46
|
@ -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
|
||||
|
||||
|
|
|
@ -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().
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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())
|
Ładowanie…
Reference in New Issue