kopia lustrzana https://gitlab.com/marnanel/chapeau
commit
6174859722
|
@ -231,9 +231,9 @@ def on_note(fields, address):
|
||||||
remote_url = fields['id'],
|
remote_url = fields['id'],
|
||||||
account = poster,
|
account = poster,
|
||||||
in_reply_to = in_reply_to,
|
in_reply_to = in_reply_to,
|
||||||
content = fields['content'],
|
content_source = fields['content'],
|
||||||
sensitive = is_sensitive,
|
sensitive = is_sensitive,
|
||||||
spoiler_text = spoiler_text,
|
spoiler_source = spoiler_text,
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
language = language,
|
language = language,
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,7 +33,7 @@ class StatusObjectSerializer(serializers.ModelSerializer):
|
||||||
'id': status.url,
|
'id': status.url,
|
||||||
'url': status.url,
|
'url': status.url,
|
||||||
'type': 'Note',
|
'type': 'Note',
|
||||||
'summary': status.spoiler_text_as_html,
|
'summary': status.spoiler_as_html,
|
||||||
'inReplyTo': status.in_reply_to,
|
'inReplyTo': status.in_reply_to,
|
||||||
'published': status.created_at,
|
'published': status.created_at,
|
||||||
'attributedTo': status.account.url,
|
'attributedTo': status.account.url,
|
||||||
|
@ -43,7 +43,7 @@ class StatusObjectSerializer(serializers.ModelSerializer):
|
||||||
'conversation': status.conversation,
|
'conversation': status.conversation,
|
||||||
'content': status.content_as_html,
|
'content': status.content_as_html,
|
||||||
'contentMap': {
|
'contentMap': {
|
||||||
status.language: status.content,
|
status.language: status.content_source,
|
||||||
},
|
},
|
||||||
'attachment': status.media_attachments,
|
'attachment': status.media_attachments,
|
||||||
'tag': status.tags,
|
'tag': status.tags,
|
||||||
|
|
|
@ -125,7 +125,7 @@ class Tests(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
original_status.content,
|
original_status.content_source,
|
||||||
'Hello world',
|
'Hello world',
|
||||||
msg = 'the status was reblogged at the end',
|
msg = 'the status was reblogged at the end',
|
||||||
)
|
)
|
||||||
|
|
|
@ -69,7 +69,7 @@ class Tests(Create_TestCase):
|
||||||
import kepi.trilby_api.models as trilby_models
|
import kepi.trilby_api.models as trilby_models
|
||||||
|
|
||||||
result = trilby_models.Status.objects.filter(
|
result = trilby_models.Status.objects.filter(
|
||||||
content = content,
|
content_source = content,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
|
|
@ -82,8 +82,22 @@ class Tests(TestCase):
|
||||||
result = trilby_models.Status(
|
result = trilby_models.Status(
|
||||||
account = self._alice,
|
account = self._alice,
|
||||||
visibility = trilby_utils.VISIBILITY_PUBLIC,
|
visibility = trilby_utils.VISIBILITY_PUBLIC,
|
||||||
content = "<p>Victoria Wood parodying Peter Skellern. I laughed so much at this, though you might have to know both singers' work in order to find it quite as funny.</p><p>- love song<br />- self-doubt<br />- refs to northern England<br />- preamble<br />- piano solo<br />- brass band<br />- choir backing<br />- love is cosy<br />- heavy rhotic vowels</p><p><a href=\"https://youtu.be/782hqdmnq7g\" rel=\"nofollow noopener\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">youtu.be/782hqdmnq7g</span><span class=\"invisible\"></span></a></p>",
|
content_source = """Victoria Wood parodying Peter Skellern.
|
||||||
)
|
I laughed so much at this, though you might have to know both singers' work
|
||||||
|
in order to find it quite as funny:
|
||||||
|
|
||||||
|
- love song
|
||||||
|
- self-doubt
|
||||||
|
- refs to northern England
|
||||||
|
- preamble
|
||||||
|
- piano solo
|
||||||
|
- brass band
|
||||||
|
- choir backing
|
||||||
|
- love is cosy
|
||||||
|
- heavy rhotic vowels
|
||||||
|
|
||||||
|
https://youtu.be/782hqdmnq7g""",
|
||||||
|
)
|
||||||
|
|
||||||
result.save()
|
result.save()
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -59,7 +59,7 @@ def on_posted(sender, **kwargs):
|
||||||
"object": {
|
"object": {
|
||||||
"type": "Note",
|
"type": "Note",
|
||||||
"id": sender.url,
|
"id": sender.url,
|
||||||
"content": sender.content,
|
"content": sender.content_as_html,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sender = sender.account,
|
sender = sender.account,
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 3.0.9 on 2021-02-16 19:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('trilby_api', '0028_mention'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='status',
|
||||||
|
old_name='spoiler_text',
|
||||||
|
new_name='spoiler_source',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='status',
|
||||||
|
name='content',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='status',
|
||||||
|
name='content_as_html_denormed',
|
||||||
|
field=models.TextField(default=None, editable=False, help_text='HTML rendering of content_source. Do not edit!', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='status',
|
||||||
|
name='content_source',
|
||||||
|
field=models.TextField(default='', help_text='Text of the status, as entered'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='status',
|
||||||
|
name='spoiler_as_html_denormed',
|
||||||
|
field=models.CharField(default=None, editable=False, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,6 +9,7 @@ logger = logging.getLogger(name='kepi')
|
||||||
|
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.db.models.constraints import UniqueConstraint
|
from django.db.models.constraints import UniqueConstraint
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -591,41 +592,52 @@ class LocalPerson(Person):
|
||||||
|
|
||||||
import kepi.trilby_api.models as trilby_models
|
import kepi.trilby_api.models as trilby_models
|
||||||
|
|
||||||
# tags aren't implemented; FIXME
|
# "Everything you're tagged in":
|
||||||
everything_youre_tagged_in = trilby_models.Status.objects.none()
|
# tags aren't implemented; FIXME
|
||||||
|
|
||||||
logger.debug("%s.inbox: tagged in: %s",
|
all_your_posts = Q(account = self)
|
||||||
self, everything_youre_tagged_in)
|
|
||||||
|
|
||||||
all_your_posts = trilby_models.Status.objects.filter(
|
# note: querysets don't get evaluated unless used,
|
||||||
account = self,
|
# so the debug logging doesn't cause a db hit
|
||||||
)
|
# unless it's actually turned on.
|
||||||
|
|
||||||
logger.debug("%s.inbox: all your posts: %s",
|
logger.debug("%s.inbox: your own posts: %s",
|
||||||
self, all_your_posts)
|
self,
|
||||||
|
trilby_models.Status.objects.filter(
|
||||||
|
all_your_posts
|
||||||
|
))
|
||||||
|
|
||||||
all_your_friends_public_posts = trilby_models.Status.objects.filter(
|
all_your_friends_public_posts = Q(
|
||||||
visibility = trilby_utils.VISIBILITY_PUBLIC,
|
visibility = trilby_utils.VISIBILITY_PUBLIC,
|
||||||
account__rel_following__following = self,
|
account__rel_followers__follower = self,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("%s.inbox: all friends' public: %s",
|
logger.debug("%s.inbox: your friends' public posts: %s",
|
||||||
self, all_your_friends_public_posts)
|
self,
|
||||||
|
trilby_models.Status.objects.filter(
|
||||||
|
all_your_friends_public_posts
|
||||||
|
))
|
||||||
|
|
||||||
all_your_mutuals_private_posts = trilby_models.Status.objects.filter(
|
all_your_mutuals_private_posts = Q(
|
||||||
visibility = trilby_utils.VISIBILITY_PRIVATE,
|
visibility = trilby_utils.VISIBILITY_PRIVATE,
|
||||||
account__rel_following__following = self,
|
account__rel_following__following = self,
|
||||||
account__rel_followers__follower = self,
|
account__rel_followers__follower = self,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("%s.inbox: all mutuals' private: %s",
|
logger.debug("%s.inbox: your mutuals' private posts: %s",
|
||||||
self, all_your_mutuals_private_posts)
|
self,
|
||||||
|
trilby_models.Status.objects.filter(
|
||||||
|
all_your_mutuals_private_posts
|
||||||
|
))
|
||||||
|
|
||||||
result = everything_youre_tagged_in.union(
|
result = trilby_models.Status.objects.filter(
|
||||||
all_your_posts,
|
all_your_posts | \
|
||||||
all_your_friends_public_posts,
|
all_your_friends_public_posts | \
|
||||||
all_your_mutuals_private_posts,
|
all_your_mutuals_private_posts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info("%s.inbox: contains %s",
|
||||||
|
self, result)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,15 @@ class Status(PolymorphicModel):
|
||||||
blank = True,
|
blank = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
content = models.TextField(
|
content_source = models.TextField(
|
||||||
|
help_text = 'Text of the status, as entered',
|
||||||
|
)
|
||||||
|
|
||||||
|
content_as_html_denormed = models.TextField(
|
||||||
|
help_text = 'HTML rendering of content_source. Do not edit!',
|
||||||
|
editable = False,
|
||||||
|
null = True,
|
||||||
|
default = None,
|
||||||
)
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(
|
created_at = models.DateTimeField(
|
||||||
|
@ -72,13 +80,20 @@ class Status(PolymorphicModel):
|
||||||
default = False,
|
default = False,
|
||||||
)
|
)
|
||||||
|
|
||||||
spoiler_text = models.CharField(
|
spoiler_source = models.CharField(
|
||||||
max_length = 255,
|
max_length = 255,
|
||||||
null = True,
|
null = True,
|
||||||
blank = True,
|
blank = True,
|
||||||
default = '',
|
default = '',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
spoiler_as_html_denormed = models.CharField(
|
||||||
|
max_length = 255,
|
||||||
|
null = True,
|
||||||
|
editable = False,
|
||||||
|
default = None,
|
||||||
|
)
|
||||||
|
|
||||||
visibility = models.CharField(
|
visibility = models.CharField(
|
||||||
max_length = 1,
|
max_length = 1,
|
||||||
default = trilby_utils.VISIBILITY_PUBLIC,
|
default = trilby_utils.VISIBILITY_PUBLIC,
|
||||||
|
@ -107,6 +122,44 @@ class Status(PolymorphicModel):
|
||||||
default = None,
|
default = None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_as_html(self):
|
||||||
|
"""
|
||||||
|
Returns an HTML rendition of content_source.
|
||||||
|
The return value will be cached.
|
||||||
|
Saving the record will clear this cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.content_as_html_denormed is not None:
|
||||||
|
return self.content_as_html_denormed
|
||||||
|
|
||||||
|
if self.content_source is None:
|
||||||
|
result = '<p></p>'
|
||||||
|
else:
|
||||||
|
result = markdown.markdown(self.content_source)
|
||||||
|
|
||||||
|
self.content_as_html_denormed = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spoiler_as_html(self):
|
||||||
|
"""
|
||||||
|
Returns an HTML rendition of spoiler_source.
|
||||||
|
The return value will be cached.
|
||||||
|
Saving the record will clear this cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.spoiler_as_html_denormed is not None:
|
||||||
|
return self.spoiler_as_html_denormed
|
||||||
|
|
||||||
|
if self.spoiler_source is None:
|
||||||
|
result = '<p></p>'
|
||||||
|
else:
|
||||||
|
result = markdown.markdown(self.spoiler_source)
|
||||||
|
|
||||||
|
self.spoiler_as_html_denormed = result
|
||||||
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def emojis(self):
|
def emojis(self):
|
||||||
return [] # TODO
|
return [] # TODO
|
||||||
|
@ -268,6 +321,19 @@ class Status(PolymorphicModel):
|
||||||
if self.in_reply_to == self:
|
if self.in_reply_to == self:
|
||||||
raise ValueError("Status can't be a reply to itself")
|
raise ValueError("Status can't be a reply to itself")
|
||||||
|
|
||||||
|
if not newly_made:
|
||||||
|
old = self.__class__.objects.get(pk=self.pk)
|
||||||
|
|
||||||
|
if self.content_source != old.content_source:
|
||||||
|
logger.debug("%s: content changed; flushing HTML cache",
|
||||||
|
self)
|
||||||
|
self.content_as_html_denormed = None
|
||||||
|
|
||||||
|
if self.spoiler_source != old.spoiler_source:
|
||||||
|
logger.debug("%s: spoiler changed; flushing HTML cache",
|
||||||
|
self)
|
||||||
|
self.spoiler_as_html_denormed = None
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if send_signal and newly_made:
|
if send_signal and newly_made:
|
||||||
|
@ -278,9 +344,9 @@ class Status(PolymorphicModel):
|
||||||
trilby_signals.reblogged.send(sender=self)
|
trilby_signals.reblogged.send(sender=self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '[Status %s: %s]' % (
|
return '%s: %s' % (
|
||||||
self.id,
|
self.id,
|
||||||
self.content,
|
self.content_source,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -350,20 +416,8 @@ class Status(PolymorphicModel):
|
||||||
# HTML and one is plain text. But the docs don't
|
# HTML and one is plain text. But the docs don't
|
||||||
# seem to be forthcoming on this point, so we'll
|
# seem to be forthcoming on this point, so we'll
|
||||||
# just have to wait until we find out.
|
# just have to wait until we find out.
|
||||||
return self.content
|
return self.content_source
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_local(self):
|
def is_local(self):
|
||||||
return self.remote_url is None
|
return self.remote_url is None
|
||||||
|
|
||||||
@property
|
|
||||||
def content_as_html(self):
|
|
||||||
if not self.content:
|
|
||||||
return '<p></p>'
|
|
||||||
return markdown.markdown(self.content)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def spoiler_text_as_html(self):
|
|
||||||
if not self.spoiler_text:
|
|
||||||
return '<p></p>'
|
|
||||||
return markdown.markdown(self.spoiler_text)
|
|
||||||
|
|
|
@ -208,26 +208,24 @@ class StatusSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
# "content" is read-only for HTML;
|
# "content" is read-only for HTML;
|
||||||
# "status" is write-only for text (or Markdown)
|
# "status" is write-only for text (or Markdown)
|
||||||
content = serializers.SerializerMethodField(
|
content = serializers.CharField(
|
||||||
|
source='content_as_html',
|
||||||
read_only = True)
|
read_only = True)
|
||||||
|
|
||||||
status = serializers.CharField(
|
status = serializers.CharField(
|
||||||
source='source_text',
|
source='content_source',
|
||||||
write_only = True)
|
write_only = True)
|
||||||
|
|
||||||
def get_content(self, status):
|
|
||||||
result = markdown.markdown(status.content)
|
|
||||||
return result
|
|
||||||
|
|
||||||
created_at = serializers.DateTimeField(
|
created_at = serializers.DateTimeField(
|
||||||
required = False,
|
required = False,
|
||||||
read_only = True)
|
read_only = True)
|
||||||
|
|
||||||
# TODO Media
|
# TODO Media
|
||||||
|
|
||||||
sensitive = serializers.BooleanField(
|
sensitive = serializers.BooleanField(
|
||||||
required = False)
|
required = False)
|
||||||
spoiler_text = serializers.CharField(
|
spoiler_text = serializers.CharField(
|
||||||
|
source='spoiler_source',
|
||||||
allow_blank = True,
|
allow_blank = True,
|
||||||
required = False)
|
required = False)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
|
# trilby_api/tests/__init__.py
|
||||||
|
#
|
||||||
|
# Part of kepi.
|
||||||
|
# Copyright (c) 2018-2021 Marnanel Thurman.
|
||||||
|
# Licensed under the GNU Public License v2.
|
||||||
|
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from rest_framework.test import force_authenticate, APIClient
|
from rest_framework.test import force_authenticate, APIClient
|
||||||
from kepi.trilby_api.models import *
|
from kepi.trilby_api.models import *
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(name='kepi')
|
||||||
|
|
||||||
ACCOUNT_EXPECTED = {
|
ACCOUNT_EXPECTED = {
|
||||||
'username': 'alice',
|
'username': 'alice',
|
||||||
'acct': 'alice',
|
'acct': 'alice',
|
||||||
|
@ -74,11 +83,11 @@ class TrilbyTestCase(TestCase):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def request(self, verb, path,
|
def request(self, verb, path,
|
||||||
data={},
|
data = None,
|
||||||
as_user=None,
|
as_user=None,
|
||||||
expect_result=200,
|
expect_result=200,
|
||||||
parse_result=True,
|
parse_result=True,
|
||||||
*args, **kwargs,
|
**extra,
|
||||||
):
|
):
|
||||||
|
|
||||||
c = APIClient()
|
c = APIClient()
|
||||||
|
@ -92,8 +101,7 @@ class TrilbyTestCase(TestCase):
|
||||||
path=path,
|
path=path,
|
||||||
data=data,
|
data=data,
|
||||||
format='json',
|
format='json',
|
||||||
*args,
|
extra=extra,
|
||||||
**kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if expect_result is not None:
|
if expect_result is not None:
|
||||||
|
@ -155,7 +163,7 @@ def create_local_status(
|
||||||
result = Status(
|
result = Status(
|
||||||
remote_url = None,
|
remote_url = None,
|
||||||
account = posted_by,
|
account = posted_by,
|
||||||
content = content,
|
content_source = content,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class TestReblog(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
reblog = create_local_status(
|
reblog = create_local_status(
|
||||||
content = original.content,
|
content = original.content_source,
|
||||||
posted_by = bob,
|
posted_by = bob,
|
||||||
reblog_of = original,
|
reblog_of = original,
|
||||||
)
|
)
|
||||||
|
@ -48,7 +48,7 @@ class TestReblog(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
reblog = create_local_status(
|
reblog = create_local_status(
|
||||||
content = original.content,
|
content = original.content_source,
|
||||||
posted_by = bob,
|
posted_by = bob,
|
||||||
reblog_of = original,
|
reblog_of = original,
|
||||||
)
|
)
|
||||||
|
@ -87,7 +87,7 @@ class TestReblog(TestCase):
|
||||||
for i in range(1, 20):
|
for i in range(1, 20):
|
||||||
|
|
||||||
reblog = create_local_status(
|
reblog = create_local_status(
|
||||||
content = original.content,
|
content = original.content_source,
|
||||||
posted_by = bob,
|
posted_by = bob,
|
||||||
reblog_of = original,
|
reblog_of = original,
|
||||||
)
|
)
|
||||||
|
|
|
@ -685,7 +685,7 @@ class TestGetStatus(TrilbyTestCase):
|
||||||
remote_url = "https://example.org/people/bob/status/100",
|
remote_url = "https://example.org/people/bob/status/100",
|
||||||
account = self._bob,
|
account = self._bob,
|
||||||
in_reply_to = self._alice_status,
|
in_reply_to = self._alice_status,
|
||||||
content = "Buttercups our gold.",
|
content_source = "Buttercups our gold.",
|
||||||
)
|
)
|
||||||
self._bob_status.save()
|
self._bob_status.save()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# test_timelines.py
|
# test_timelines.py
|
||||||
#
|
#
|
||||||
# Part of kepi.
|
# Part of kepi.
|
||||||
# Copyright (c) 2018-2020 Marnanel Thurman.
|
# Copyright (c) 2018-2021 Marnanel Thurman.
|
||||||
# Licensed under the GNU Public License v2.
|
# Licensed under the GNU Public License v2.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -11,123 +11,515 @@ from rest_framework.test import APIClient, force_authenticate
|
||||||
from kepi.trilby_api.views import *
|
from kepi.trilby_api.views import *
|
||||||
from kepi.trilby_api.tests import *
|
from kepi.trilby_api.tests import *
|
||||||
from kepi.trilby_api.models import *
|
from kepi.trilby_api.models import *
|
||||||
|
from kepi.bowler_pub.tests import create_remote_person
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
|
import httpretty
|
||||||
|
|
||||||
# Tests for timelines. API docs are here:
|
"""
|
||||||
# https://docs.joinmastodon.org/methods/statuses/
|
Tests for timelines. API docs are here:
|
||||||
|
https://docs.joinmastodon.org/methods/timelines/
|
||||||
|
"""
|
||||||
|
|
||||||
TIMELINE_DATA = [
|
class TimelineTestCase(TrilbyTestCase):
|
||||||
# Visibility is:
|
|
||||||
# A=public: visible to anyone, and in public timelines
|
|
||||||
# U=unlisted: visible to anyone, but not in public timelines
|
|
||||||
# X=private: visible to followers and anyone tagged
|
|
||||||
# D=direct: visible only to those who are tagged
|
|
||||||
|
|
||||||
# We haven't yet implemented:
|
def add_status(self, source, visibility, content,
|
||||||
# - (user) tags
|
remote_url = None):
|
||||||
# - hashtags
|
status = Status(
|
||||||
# - user lists
|
account = source,
|
||||||
# - following users but hiding reblogs
|
content_source = content,
|
||||||
# and when we do, these tests will need updating.
|
visibility = visibility,
|
||||||
#
|
remote_url = remote_url,
|
||||||
# All statuses are posted by alice.
|
)
|
||||||
#
|
status.save()
|
||||||
# id visibility visible in
|
|
||||||
( 'A', 'A',
|
|
||||||
['public', 'follower', 'stranger', 'home', ], ),
|
|
||||||
( 'B', 'U',
|
|
||||||
['follower', 'stranger', 'home', ], ),
|
|
||||||
( 'C', 'X',
|
|
||||||
['follower', 'home',], ),
|
|
||||||
( 'D', 'D',
|
|
||||||
['home', ], ),
|
|
||||||
|
|
||||||
]
|
logger.info("Created status: %s", status)
|
||||||
|
|
||||||
class TestTimelines(TrilbyTestCase):
|
return status
|
||||||
|
|
||||||
def _set_up(self):
|
def timeline_contents(self,
|
||||||
|
|
||||||
self._alice = create_local_person("alice")
|
|
||||||
|
|
||||||
for (id, visibility, visible_in) in TIMELINE_DATA:
|
|
||||||
status = Status(
|
|
||||||
account = self._alice,
|
|
||||||
content = id,
|
|
||||||
visibility = visibility,
|
|
||||||
)
|
|
||||||
status.save()
|
|
||||||
|
|
||||||
def _check_timelines(self,
|
|
||||||
situation,
|
|
||||||
path,
|
path,
|
||||||
as_user):
|
data = None,
|
||||||
|
|
||||||
expected = []
|
|
||||||
for (id, visibility, visible_in) in TIMELINE_DATA:
|
|
||||||
if situation in visible_in:
|
|
||||||
expected.append(f'<p>{id}</p>')
|
|
||||||
expected = sorted(expected)
|
|
||||||
|
|
||||||
details = sorted([x['content'] \
|
|
||||||
for x in self.get(
|
|
||||||
path = path,
|
|
||||||
as_user = as_user,
|
|
||||||
)])
|
|
||||||
|
|
||||||
self.assertListEqual(
|
|
||||||
expected,
|
|
||||||
details,
|
|
||||||
msg = f"Visibility in '{situation}' mismatch: "+\
|
|
||||||
f"expected {expected}, but got {details}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_public(self):
|
|
||||||
self._set_up()
|
|
||||||
|
|
||||||
self._check_timelines(
|
|
||||||
situation = 'public',
|
|
||||||
path = '/api/v1/timelines/public',
|
|
||||||
as_user = None,
|
as_user = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
logger.info("Timeline contents for %s as %s...",
|
||||||
|
path,
|
||||||
|
as_user)
|
||||||
|
|
||||||
|
found = self.get(
|
||||||
|
path = path,
|
||||||
|
data = data,
|
||||||
|
as_user = as_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(" -- retrieved")
|
||||||
|
|
||||||
|
details = sorted([x['content'] for x in found])
|
||||||
|
|
||||||
|
logger.debug(" -- sorted as %s",
|
||||||
|
details)
|
||||||
|
|
||||||
|
result = ''
|
||||||
|
for detail in details:
|
||||||
|
|
||||||
|
if detail.startswith('<p>') and detail.endswith('</p>'):
|
||||||
|
detail = detail[3:-4]
|
||||||
|
|
||||||
|
result += detail
|
||||||
|
|
||||||
|
logger.info(" -- contents are %s",
|
||||||
|
result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
class TestPublicTimeline(TimelineTestCase):
|
||||||
|
|
||||||
|
def test_as_anon(self):
|
||||||
|
|
||||||
|
alice = create_local_person("alice")
|
||||||
|
|
||||||
|
self.add_status(source=alice, content='A', visibility='A')
|
||||||
|
self.add_status(source=alice, content='B', visibility='U')
|
||||||
|
self.add_status(source=alice, content='C', visibility='X')
|
||||||
|
self.add_status(source=alice, content='D', visibility='D')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
as_user = None,
|
||||||
|
),
|
||||||
|
'A',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_follower(self):
|
def test_as_user(self):
|
||||||
self._set_up()
|
|
||||||
self._george = create_local_person("george")
|
alice = create_local_person("alice")
|
||||||
|
|
||||||
|
self.add_status(source=alice, content='A', visibility='A')
|
||||||
|
self.add_status(source=alice, content='B', visibility='U')
|
||||||
|
self.add_status(source=alice, content='C', visibility='X')
|
||||||
|
self.add_status(source=alice, content='D', visibility='D')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
as_user = alice,
|
||||||
|
),
|
||||||
|
'A',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_as_stranger(self):
|
||||||
|
|
||||||
|
alice = create_local_person("alice")
|
||||||
|
henry = create_local_person("henry")
|
||||||
|
|
||||||
|
self.add_status(source=alice, content='A', visibility='A')
|
||||||
|
self.add_status(source=alice, content='B', visibility='U')
|
||||||
|
self.add_status(source=alice, content='C', visibility='X')
|
||||||
|
self.add_status(source=alice, content='D', visibility='D')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
as_user = henry,
|
||||||
|
),
|
||||||
|
'A',
|
||||||
|
)
|
||||||
|
|
||||||
|
@httpretty.activate()
|
||||||
|
def test_local_and_remote(self):
|
||||||
|
|
||||||
|
alice = create_local_person("alice")
|
||||||
|
peter = create_remote_person(
|
||||||
|
remote_url = "https://example.com/users/peter",
|
||||||
|
name = "peter",
|
||||||
|
auto_fetch = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_status(source=alice, content='A', visibility='A')
|
||||||
|
self.add_status(source=peter, content='B', visibility='A',
|
||||||
|
remote_url = 'https://example.com/users/peter/B')
|
||||||
|
self.add_status(source=alice, content='C', visibility='A')
|
||||||
|
self.add_status(source=peter, content='D', visibility='A',
|
||||||
|
remote_url = 'https://example.com/users/peter/D')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
),
|
||||||
|
'ABCD',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'local': 'true'},
|
||||||
|
),
|
||||||
|
'AC',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'local': 'false'},
|
||||||
|
),
|
||||||
|
'ABCD',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'remote': 'true'},
|
||||||
|
),
|
||||||
|
'BD',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'remote': 'false'},
|
||||||
|
),
|
||||||
|
'ABCD',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'local': 'true', 'remote': 'true'},
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_only_media(self):
|
||||||
|
|
||||||
|
# We don't support added media at present anyway,
|
||||||
|
# so turning this on will always get the empty set
|
||||||
|
|
||||||
|
alice = create_local_person("alice")
|
||||||
|
|
||||||
|
self.add_status(source=alice, content='A', visibility='A')
|
||||||
|
self.add_status(source=alice, content='B', visibility='A')
|
||||||
|
self.add_status(source=alice, content='C', visibility='A')
|
||||||
|
self.add_status(source=alice, content='D', visibility='A')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'only_media': 'true'},
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_max_since_and_min(self):
|
||||||
|
|
||||||
|
alice = create_local_person("alice")
|
||||||
|
|
||||||
|
self.add_status(source=alice, content='A', visibility='A')
|
||||||
|
self.add_status(source=alice, content='B', visibility='A')
|
||||||
|
status_c = self.add_status(source=alice, content='C', visibility='A')
|
||||||
|
self.add_status(source=alice, content='D', visibility='A')
|
||||||
|
|
||||||
|
c_id = str(status_c.id)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'since_id': status_c.id},
|
||||||
|
),
|
||||||
|
'D',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'max_id': status_c.id},
|
||||||
|
),
|
||||||
|
'ABC',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'min_id': status_c.id},
|
||||||
|
),
|
||||||
|
'CD',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_limit(self):
|
||||||
|
|
||||||
|
alice = create_local_person("alice")
|
||||||
|
|
||||||
|
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
|
||||||
|
for i in range(len(alphabet)):
|
||||||
|
self.add_status(
|
||||||
|
source=alice,
|
||||||
|
content=alphabet[i],
|
||||||
|
visibility='A',
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(1, len(alphabet)):
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
data = {'limit': i},
|
||||||
|
),
|
||||||
|
alphabet[:i],
|
||||||
|
)
|
||||||
|
|
||||||
|
# the default is specified as 20
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/public',
|
||||||
|
),
|
||||||
|
alphabet[:20],
|
||||||
|
msg = 'default is 20',
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestHomeTimeline(TimelineTestCase):
|
||||||
|
|
||||||
|
def add_standard_statuses(self):
|
||||||
|
self.alice = create_local_person("alice")
|
||||||
|
self.bob = create_local_person("bob")
|
||||||
|
self.carol = create_local_person("carol")
|
||||||
|
|
||||||
|
self.add_status(source=self.bob, content='A', visibility='A')
|
||||||
|
self.add_status(source=self.carol, content='B', visibility='A')
|
||||||
|
self.add_status(source=self.carol, content='C', visibility='A')
|
||||||
|
self.add_status(source=self.bob, content='D', visibility='A')
|
||||||
|
|
||||||
|
Follow(
|
||||||
|
follower=self.alice,
|
||||||
|
following=self.bob,
|
||||||
|
offer=None).save()
|
||||||
|
|
||||||
|
def follow_carol(self):
|
||||||
|
Follow(
|
||||||
|
follower=self.alice,
|
||||||
|
following=self.carol,
|
||||||
|
offer=None).save()
|
||||||
|
|
||||||
|
def test_not_anon(self):
|
||||||
|
found = self.get(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
as_user = None,
|
||||||
|
expect_result = 401,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_0_simple(self):
|
||||||
|
|
||||||
|
self.add_standard_statuses()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'AD',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.follow_carol()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'ABCD',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_max_since_and_min(self):
|
||||||
|
|
||||||
|
self.add_standard_statuses()
|
||||||
|
|
||||||
|
c_id = '3' # FIXME hack
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'since_id': c_id},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'D',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'max_id': c_id},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'A',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'min_id': c_id},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'D',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.follow_carol()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'since_id': c_id},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'D',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'max_id': c_id},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'ABC',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'min_id': c_id},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'CD',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_limit(self):
|
||||||
|
|
||||||
|
self.alice = create_local_person("alice")
|
||||||
|
self.bob = create_local_person("bob")
|
||||||
|
self.carol = create_local_person("carol")
|
||||||
|
|
||||||
|
Follow(
|
||||||
|
follower=self.alice,
|
||||||
|
following=self.bob,
|
||||||
|
offer=None).save()
|
||||||
|
|
||||||
|
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
|
||||||
|
for i in range(len(alphabet)):
|
||||||
|
self.add_status(
|
||||||
|
source=self.bob,
|
||||||
|
content=alphabet[i],
|
||||||
|
visibility='A',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_status(
|
||||||
|
source=self.carol,
|
||||||
|
content=alphabet[i].lower(),
|
||||||
|
visibility='A',
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(1, len(alphabet)):
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'limit': i},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
alphabet[:i],
|
||||||
|
)
|
||||||
|
|
||||||
|
# the default is specified as 20
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
alphabet[:20],
|
||||||
|
msg = 'default is 20',
|
||||||
|
)
|
||||||
|
|
||||||
|
@httpretty.activate()
|
||||||
|
def test_local(self):
|
||||||
|
|
||||||
|
self.add_standard_statuses()
|
||||||
|
|
||||||
|
self.peter = create_remote_person(
|
||||||
|
remote_url = "https://example.com/users/peter",
|
||||||
|
name = "peter",
|
||||||
|
auto_fetch = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for letter in 'PQ':
|
||||||
|
self.add_status(source=self.peter,
|
||||||
|
remote_url = 'https://example.com/users/peter/{}'.format(
|
||||||
|
letter,
|
||||||
|
),
|
||||||
|
content=letter,
|
||||||
|
visibility='A')
|
||||||
|
|
||||||
|
Follow(
|
||||||
|
follower = self.alice,
|
||||||
|
following = self.peter,
|
||||||
|
offer = None,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'ADPQ',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
data = {'local': 'true'},
|
||||||
|
as_user = self.alice,
|
||||||
|
),
|
||||||
|
'AD',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_as_follower(self):
|
||||||
|
|
||||||
|
alice = create_local_person("alice")
|
||||||
|
george = create_local_person("george")
|
||||||
|
|
||||||
follow = Follow(
|
follow = Follow(
|
||||||
follower = self._george,
|
follower = george,
|
||||||
following = self._alice,
|
following = alice,
|
||||||
offer = None,
|
offer = None,
|
||||||
)
|
)
|
||||||
follow.save()
|
follow.save()
|
||||||
|
|
||||||
self._check_timelines(
|
self.add_status(source=alice, content='A', visibility='A')
|
||||||
situation = 'public',
|
self.add_status(source=alice, content='B', visibility='U')
|
||||||
path = '/api/v1/timelines/public',
|
self.add_status(source=alice, content='C', visibility='X')
|
||||||
as_user = self._george,
|
self.add_status(source=alice, content='D', visibility='D')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.timeline_contents(
|
||||||
|
path = '/api/v1/timelines/home',
|
||||||
|
as_user = george,
|
||||||
|
),
|
||||||
|
'A',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_stranger(self):
|
follow = Follow(
|
||||||
self._set_up()
|
follower = alice,
|
||||||
self._henry = create_local_person("henry")
|
following = george,
|
||||||
|
offer = None,
|
||||||
|
)
|
||||||
|
follow.save() # they are now mutuals
|
||||||
|
|
||||||
self._check_timelines(
|
self.assertEqual(
|
||||||
situation = 'public',
|
self.timeline_contents(
|
||||||
path = '/api/v1/timelines/public',
|
path = '/api/v1/timelines/home',
|
||||||
as_user = self._henry,
|
as_user = george,
|
||||||
)
|
),
|
||||||
|
'AC',
|
||||||
def test_home(self):
|
|
||||||
self._set_up()
|
|
||||||
|
|
||||||
self._check_timelines(
|
|
||||||
situation = 'home',
|
|
||||||
path = '/api/v1/timelines/home',
|
|
||||||
as_user = self._alice,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class TestTimelinesNotImplemented(TimelineTestCase):
|
||||||
@skip("to be implemented later")
|
@skip("to be implemented later")
|
||||||
def test_hashtag(self):
|
def test_hashtag(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
|
@ -135,18 +135,18 @@ class Reblog(DoSomethingWithStatus):
|
||||||
# https://github.com/tootsuite/mastodon/issues/13479
|
# https://github.com/tootsuite/mastodon/issues/13479
|
||||||
# For now, I'm assuming that you can.
|
# For now, I'm assuming that you can.
|
||||||
|
|
||||||
content = 'RT {}'.format(the_status.content)
|
content_source = 'RT {}'.format(the_status.content_source)
|
||||||
|
|
||||||
new_status = trilby_models.Status(
|
new_status = trilby_models.Status(
|
||||||
|
|
||||||
# Fields which are different in a reblog:
|
# Fields which are different in a reblog:
|
||||||
account = request.user.localperson,
|
account = request.user.localperson,
|
||||||
content = content,
|
content_source = content_source,
|
||||||
reblog_of = the_status,
|
reblog_of = the_status,
|
||||||
|
|
||||||
# Fields which are just copied in:
|
# Fields which are just copied in:
|
||||||
sensitive = the_status.sensitive,
|
sensitive = the_status.sensitive,
|
||||||
spoiler_text = the_status.spoiler_text,
|
spoiler_source = the_status.spoiler_source,
|
||||||
visibility = the_status.visibility,
|
visibility = the_status.visibility,
|
||||||
language = the_status.language,
|
language = the_status.language,
|
||||||
in_reply_to = the_status.in_reply_to,
|
in_reply_to = the_status.in_reply_to,
|
||||||
|
@ -397,9 +397,9 @@ class Statuses(generics.ListCreateAPIView,
|
||||||
|
|
||||||
status = trilby_models.Status(
|
status = trilby_models.Status(
|
||||||
account = request.user.localperson,
|
account = request.user.localperson,
|
||||||
content = data.get('status', ''),
|
content_source = data.get('status', ''),
|
||||||
sensitive = data.get('sensitive', False),
|
sensitive = data.get('sensitive', False),
|
||||||
spoiler_text = data.get('spoiler_text', ''),
|
spoiler_source = data.get('spoiler_text', ''),
|
||||||
visibility = data.get('visibility', 'public'),
|
visibility = data.get('visibility', 'public'),
|
||||||
language = data.get('language',
|
language = data.get('language',
|
||||||
settings.KEPI['LANGUAGES'][0]),
|
settings.KEPI['LANGUAGES'][0]),
|
||||||
|
|
|
@ -31,6 +31,7 @@ import json
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
DEFAULT_TIMELINE_SLICE_LENGTH = 20
|
||||||
|
|
||||||
class AbstractTimeline(generics.ListAPIView):
|
class AbstractTimeline(generics.ListAPIView):
|
||||||
|
|
||||||
|
@ -39,29 +40,82 @@ class AbstractTimeline(generics.ListAPIView):
|
||||||
IsAuthenticated,
|
IsAuthenticated,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self):
|
||||||
raise NotImplementedError("cannot query abstract timeline")
|
raise NotImplementedError("cannot query abstract timeline")
|
||||||
|
|
||||||
def get(self, request):
|
def filter_queryset(self, queryset,
|
||||||
queryset = self.get_queryset(request)
|
min_id = None,
|
||||||
serializer = self.serializer_class(queryset,
|
max_id = None,
|
||||||
many = True,
|
since_id = None,
|
||||||
context = {
|
local = False,
|
||||||
'request': request,
|
remote = False,
|
||||||
})
|
limit = DEFAULT_TIMELINE_SLICE_LENGTH,
|
||||||
return Response(serializer.data)
|
*args, **kwargs,
|
||||||
|
):
|
||||||
|
|
||||||
PUBLIC_TIMELINE_SLICE_LENGTH = 20
|
logger.debug("Timeline queryset: %s", queryset)
|
||||||
|
|
||||||
|
if 'min_id' in self.request.query_params:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
id__gte = int(self.request.query_params['min_id']),
|
||||||
|
)
|
||||||
|
logger.debug(" -- after min_id: %s", queryset)
|
||||||
|
|
||||||
|
if 'max_id' in self.request.query_params:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
id__lte = int(self.request.query_params['max_id']),
|
||||||
|
)
|
||||||
|
logger.debug(" -- after max_id: %s", queryset)
|
||||||
|
|
||||||
|
if 'since_id' in self.request.query_params:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
id__gt = int(self.request.query_params['since_id']),
|
||||||
|
)
|
||||||
|
logger.debug(" -- after since_id: %s", queryset)
|
||||||
|
|
||||||
|
if self.request.query_params.get('local', '')=='true':
|
||||||
|
queryset = queryset.filter(
|
||||||
|
remote_url__isnull = True,
|
||||||
|
)
|
||||||
|
logger.debug(" -- after local: %s", queryset)
|
||||||
|
|
||||||
|
if self.request.query_params.get('remote', '')=='true':
|
||||||
|
queryset = queryset.filter(
|
||||||
|
remote_url__isnull = False,
|
||||||
|
)
|
||||||
|
logger.debug(" -- after remote: %s", queryset)
|
||||||
|
|
||||||
|
if 'only_media' in self.request.query_params:
|
||||||
|
# We don't support media at present, so this will give us
|
||||||
|
# the empty set
|
||||||
|
queryset = queryset.none()
|
||||||
|
logger.debug(" -- after only_media: %s", queryset)
|
||||||
|
|
||||||
|
# Slicing the queryset must be done last,
|
||||||
|
# since running operations on a sliced queryset
|
||||||
|
# causes evaluation.
|
||||||
|
limit = int(self.request.query_params.get('limit',
|
||||||
|
default = DEFAULT_TIMELINE_SLICE_LENGTH,
|
||||||
|
))
|
||||||
|
|
||||||
|
queryset = queryset[:limit]
|
||||||
|
|
||||||
|
logger.debug(" -- after slice of %d: %s",
|
||||||
|
limit,
|
||||||
|
queryset,
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
class PublicTimeline(AbstractTimeline):
|
class PublicTimeline(AbstractTimeline):
|
||||||
|
|
||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self):
|
||||||
|
|
||||||
result = trilby_models.Status.objects.filter(
|
result = trilby_models.Status.objects.filter(
|
||||||
visibility = trilby_utils.VISIBILITY_PUBLIC,
|
visibility = trilby_utils.VISIBILITY_PUBLIC,
|
||||||
)[:PUBLIC_TIMELINE_SLICE_LENGTH]
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -71,9 +125,9 @@ class HomeTimeline(AbstractTimeline):
|
||||||
IsAuthenticated,
|
IsAuthenticated,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self):
|
||||||
|
|
||||||
result = request.user.localperson.inbox
|
result = self.request.user.localperson.inbox
|
||||||
|
|
||||||
logger.debug("Home timeline is %s",
|
logger.debug("Home timeline is %s",
|
||||||
result)
|
result)
|
||||||
|
@ -82,8 +136,6 @@ class HomeTimeline(AbstractTimeline):
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
|
|
||||||
########################################
|
|
||||||
|
|
||||||
class UserFeed(View):
|
class UserFeed(View):
|
||||||
|
|
||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
|
|
Ładowanie…
Reference in New Issue