chapeau/chapeau/bowler_pub/models/acobject.py

455 wiersze
12 KiB
Python

from django.db import models, IntegrityError
from django.conf import settings
from django.utils import timezone
from polymorphic.models import PolymorphicModel
from polymorphic.managers import PolymorphicManager
from chapeau.bowler_pub.models.audience import Audience, AUDIENCE_FIELD_NAMES
from chapeau.bowler_pub.models.thingfield import ThingField
from chapeau.bowler_pub.models.mention import Mention
from chapeau.bowler_pub.utils import configured_path, uri_to_url
from .. import URL_REGEXP, SERIAL_NUMBER_REGEXP
import chapeau.bowler_pub.side_effects as side_effects
import logging
import random
import warnings
import re
logger = logging.getLogger(name='chapeau')
######################
class KepiManager(PolymorphicManager):
# TODO: This should allow filtering on names
# without their f_... prefixes, and also
# transparently on ThingFields.
def filter_local_only(self, *args, **kwargs):
self._adjust_kwargs_for_local_only(kwargs)
return self.filter(*args, **kwargs)
def get_local_only(self, *args, **kwargs):
self._adjust_kwargs_for_local_only(kwargs)
return self.get(*args, **kwargs)
def _adjust_kwargs_for_local_only(self, kwargs):
LOCAL_ONLY = 'id__regex'
if LOCAL_ONLY in kwargs:
raise ValueError(('%s is already an argument; '+\
'this should never happen') % (LOCAL_ONLY,))
kwargs[LOCAL_ONLY] = r'^[/@]'
######################
class AcObject(PolymorphicModel):
id = models.CharField(
max_length=255,
primary_key=True,
unique=True,
blank=False,
default=None,
editable=False,
)
objects = KepiManager()
published = models.DateTimeField(
default = timezone.now,
)
updated = models.DateTimeField(
auto_now = True,
)
@property
def url(self):
uri = self.uri
if uri is None:
return self.id
else:
return uri_to_url(uri)
@property
def uri(self):
if self.id.startswith('/'):
return configured_path('OBJECT_LINK',
number = self.id[1:],
)
elif self.id.startswith('@'):
return configured_path('USER_LINK',
username = self.id[1:],
)
else:
return None
@property
def number(self):
if self.is_local:
return self.id[1:]
else:
return None
def __str__(self):
result = '[%s %s]' % (
self.id,
self.f_type,
)
return result
@property
def short_id(self):
return self.id
@property
def pretty(self):
result = ''
curly = '{'
items = [
('type', self.f_type),
]
for f, v in sorted(self.activity_form.items()):
if f in ['type']:
continue
items.append( (f,v) )
items.extend( [
('actor', self.f_actor),
] )
for f, v in items:
if not v:
continue
if result:
result += ',\n'
result += '%1s %15s: %s' % (
curly,
f,
v,
)
curly = ''
result += ' }'
return result
@property
def f_type(self):
return self.__class__.__name__[2:]
@property
def activity_form(self):
from chapeau.bowler_pub.utils import short_id_to_url
result = {
'id': self.url,
'type': self.f_type,
'published': self.published,
}
for name in dir(self):
if not name.startswith('f_'):
continue
value = getattr(self, name)
value = short_id_to_url(value)
if not isinstance(value, str):
continue
if value=='':
value = None
result[name[2:]] = value
result.update(ThingField.get_fields_for(self))
result.update(Audience.get_audiences_for(self))
return result
def __contains__(self, name):
try:
self.__getitem__(name)
return True
except:
return False
def __getitem__(self, name):
from chapeau.bowler_pub.find import find
name_parts = name.split('__')
name = name_parts.pop(0)
if hasattr(self, 'f_'+name):
result = getattr(self, 'f_'+name)
elif name in [
'published',
'updated',
'url',
'id',
]:
result = getattr(self, name)
elif name in AUDIENCE_FIELD_NAMES:
try:
result = Audience.get_audiences_for(
thing = self,
audience_type = name,
)
except Audience.DoesNotExist:
result = None
else:
try:
another = ThingField.objects.get(
parent = self,
field = name)
if 'raw' in name_parts:
result = another.value
else:
result = another.interpreted_value
except ThingField.DoesNotExist:
result = None
if 'find' in name_parts and result is not None:
result = find(result,
do_not_fetch=True)
elif 'obj' in name_parts and result is not None:
result = AcObject.get_by_url(url=result)
return result
def __setitem__(self, name, value):
value = _normalise_type_for_thing(value)
logger.debug(' -- %8s %12s %s',
self.id,
name,
value,
)
if hasattr(self, 'f_'+name):
setattr(self, 'f_'+name, value)
self.save()
elif name in [
'published',
]:
setattr(self, name, value)
self.save()
elif name in AUDIENCE_FIELD_NAMES:
if self.pk is None:
# We *must* save at this point;
# otherwise Audience might have no foreign key.
self.save()
Audience.add_audiences_for(
thing = self,
field = name,
value = value,
)
else:
from chapeau.bowler_pub.utils import as_json
if self.pk is None:
# See above
self.save()
try:
another = ThingField.objects.get(
parent = self,
field = name,
)
except ThingField.DoesNotExist:
another = ThingField(
parent = self,
field = name,
)
another.value = as_json(value)
another.save()
# Special-cased side effects:
if name=='tag':
# We must save, in order for Mention's fk to point to us
self.save()
Mention.set_from_tags(
status=self,
tags=value,
)
@property
def audiences(self):
return Audience.get_audiences_for(self)
def run_side_effects(self):
from chapeau.bowler_pub.find import find
from chapeau.bowler_pub.delivery import deliver
f_type = self.f_type.lower()
if not hasattr(side_effects, f_type):
logger.debug(' -- no side effects for %s',
f_type)
return True
result = getattr(side_effects, f_type)(self)
return result
@property
def is_local(self):
return self.id and self.id[0] in '@/'
def entomb(self):
logger.info('%s: entombing', self)
if self['former_type'] is not None:
logger.warn(' -- already entombed; ignoring')
return
if not self.is_local:
raise ValueError("%s: you can't entomb remote things",
self)
self['former_type'] = self.f_type
self['deleted'] = timezone.now()
self.save()
logger.info('%s: entombed', self)
def _generate_id(self):
"""
Returns a value for "id" on a new object, where
the caller has omitted to supply an "id" value.
The new value should be unique.
If this method returns None, the object will
not be created.
"""
return '/%08x' % (random.randint(0, 0xffffffff),)
def _check_provided_id(self):
"""
Checks self.id to see whether it's valid for
this kind of AcObject. It may normalise the value.
If the value is valid, returns.
If the value is invalid, raises ValueError.
This method is not called if self.id is a valid
URL, because that means it's a remote object
and our naming rules won't apply.
"""
if re.match(SERIAL_NUMBER_REGEXP, self.id,
re.IGNORECASE):
self.id = self.id.lower()
logger.debug('id==%s which is a valid serial number',
self.id)
return
raise ValueError("Object IDs begin with a slash "+\
"followed by eight characters from "+\
"0-9 or a-f. "+\
"You gave: "+self.id)
def save(self, *args, **kwargs):
if self.id is None:
self.id = self._generate_id()
if self.id is None:
raise ValueError("You need to specify an id "+\
"on %s objects." % (self.__class__.__name__,))
else:
if re.match(URL_REGEXP, self.id,
re.IGNORECASE):
logger.debug('id==%s which is a valid URL',
self.id)
else:
self._check_provided_id()
try:
super().save(*args, **kwargs)
logger.debug('%s: saved', self)
except IntegrityError as ie:
logger.info('Integrity error on save (%s); failed',
ie)
raise ie
@classmethod
def get_by_url(cls, url):
"""
Retrieves an AcObject whose URL is "url".
This differs from find() in that it can
find objects which were submitted to us over HTTP
(as opposed to generated locally or fetched by us).
find() would ignore these.
"""
from chapeau.bowler_pub.find import find
logger.debug(' -- finding object with url %s', url)
result = find(url,
local_only = True)
if result is None:
logger.debug(' -- not local; trying remote')
try:
result = cls.objects.get(
id = url,
)
except cls.DoesNotExist:
result = None
logger.debug(' -- found %s', result)
return result
######################################
def _normalise_type_for_thing(v):
if v is None:
return v # we're good with nulls
if isinstance(v, str):
return v # strings are fine
elif isinstance(v, dict):
return v # so are dicts
elif isinstance(v, bool):
return v # also booleans
elif isinstance(v, list):
return v # and lists as well
elif isinstance(v, AcObject):
return v.short_id # AcObjects can deal with themselves
# okay, it's something weird
try:
return v.activity_form
except AttributeError:
return str(v)