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):
|
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
|
||||||
|
|
||||||
|
|
|
@ -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().
|
||||||
|
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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