mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-23 00:26:33 +00:00
Followers and following lists
This commit is contained in:
parent
77bab24834
commit
a9d938fbb2
6 changed files with 169 additions and 39 deletions
|
@ -32,6 +32,12 @@ def webfinger(request):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
def host_meta(request):
|
||||||
|
import pdb;pdb.set_trace()
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def shared_inbox(request):
|
def shared_inbox(request):
|
||||||
''' incoming activitypub events '''
|
''' incoming activitypub events '''
|
||||||
|
@ -104,6 +110,7 @@ def get_actor(request, username):
|
||||||
'id': user.actor,
|
'id': user.actor,
|
||||||
'type': 'Person',
|
'type': 'Person',
|
||||||
'preferredUsername': user.localname,
|
'preferredUsername': user.localname,
|
||||||
|
'name': user.name,
|
||||||
'inbox': user.inbox,
|
'inbox': user.inbox,
|
||||||
'followers': '%s/followers' % user.actor,
|
'followers': '%s/followers' % user.actor,
|
||||||
'following': '%s/following' % user.actor,
|
'following': '%s/following' % user.actor,
|
||||||
|
@ -118,6 +125,73 @@ def get_actor(request, username):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def get_followers(request, username):
|
||||||
|
''' return a list of followers for an actor '''
|
||||||
|
if request.method != 'GET':
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
user = models.User.objects.get(localname=username)
|
||||||
|
followers = user.followers
|
||||||
|
id_slug = '%s/followers' % user.actor
|
||||||
|
if request.GET.get('page'):
|
||||||
|
page = request.GET.get('page')
|
||||||
|
return JsonResponse(get_follow_page(followers, id_slug, page))
|
||||||
|
follower_count = followers.count()
|
||||||
|
return JsonResponse({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': id_slug,
|
||||||
|
'type': 'OrderedCollection',
|
||||||
|
'totalItems': follower_count,
|
||||||
|
'first': '%s?page=1' % id_slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def get_following(request, username):
|
||||||
|
''' return a list of following for an actor '''
|
||||||
|
# TODO: this is total deplication of get_followers, should be streamlined
|
||||||
|
if request.method != 'GET':
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
user = models.User.objects.get(localname=username)
|
||||||
|
following = models.User.objects.filter(followers=user)
|
||||||
|
id_slug = '%s/following' % user.actor
|
||||||
|
if request.GET.get('page'):
|
||||||
|
page = request.GET.get('page')
|
||||||
|
return JsonResponse(get_follow_page(following, id_slug, page))
|
||||||
|
following_count = following.count()
|
||||||
|
return JsonResponse({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': id_slug,
|
||||||
|
'type': 'OrderedCollection',
|
||||||
|
'totalItems': following_count,
|
||||||
|
'first': '%s?page=1' % id_slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def get_follow_page(user_list, id_slug, page):
|
||||||
|
''' format a list of followers/following '''
|
||||||
|
page = int(page)
|
||||||
|
page_length = 10
|
||||||
|
start = (page - 1) * page_length
|
||||||
|
end = start + page_length
|
||||||
|
follower_page = user_list.all()[start:end]
|
||||||
|
data = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': '%s?page=%d' % (id_slug, page),
|
||||||
|
'type': 'OrderedCollectionPage',
|
||||||
|
'totalItems': user_list.count(),
|
||||||
|
'partOf': id_slug,
|
||||||
|
'orderedItems': [u.actor for u in follower_page],
|
||||||
|
}
|
||||||
|
if end <= user_list.count():
|
||||||
|
# there are still more pages
|
||||||
|
data['next'] = '%s?page=%d' % (id_slug, page + 1)
|
||||||
|
if start > 0:
|
||||||
|
data['prev'] = '%s?page=%d' % (id_slug, page - 1)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def handle_incoming_shelve(activity):
|
def handle_incoming_shelve(activity):
|
||||||
''' receiving an Add activity (to shelve a book) '''
|
''' receiving an Add activity (to shelve a book) '''
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 3.0.2 on 2020-01-29 09:04
|
# Generated by Django 3.0.2 on 2020-01-29 18:40
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
|
@ -41,14 +41,11 @@ class Migration(migrations.Migration):
|
||||||
('outbox', models.CharField(max_length=255, unique=True)),
|
('outbox', models.CharField(max_length=255, unique=True)),
|
||||||
('summary', models.TextField(blank=True, null=True)),
|
('summary', models.TextField(blank=True, null=True)),
|
||||||
('local', models.BooleanField(default=True)),
|
('local', models.BooleanField(default=True)),
|
||||||
('localname', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
('localname', models.CharField(max_length=255, null=True, unique=True)),
|
||||||
('name', models.CharField(blank=True, max_length=100, null=True)),
|
('name', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
('followers', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
|
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'user',
|
'verbose_name': 'user',
|
||||||
|
@ -96,6 +93,16 @@ class Migration(migrations.Migration):
|
||||||
('authors', models.ManyToManyField(to='fedireads.Author')),
|
('authors', models.ManyToManyField(to='fedireads.Author')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FederatedServer',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('server_name', models.CharField(max_length=255, unique=True)),
|
||||||
|
('shared_inbox', models.CharField(max_length=255, unique=True)),
|
||||||
|
('status', models.CharField(default='federated', max_length=255)),
|
||||||
|
('application_type', models.CharField(max_length=255, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Shelf',
|
name='Shelf',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -144,6 +151,26 @@ class Migration(migrations.Migration):
|
||||||
name='shelves',
|
name='shelves',
|
||||||
field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Shelf'),
|
field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Shelf'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='federated_server',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.FederatedServer'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='followers',
|
||||||
|
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='groups',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='user_permissions',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ShelveActivity',
|
name='ShelveActivity',
|
||||||
fields=[
|
fields=[
|
||||||
|
|
|
@ -19,19 +19,22 @@ class User(AbstractUser):
|
||||||
actor = models.CharField(max_length=255, unique=True)
|
actor = models.CharField(max_length=255, unique=True)
|
||||||
inbox = models.CharField(max_length=255, unique=True)
|
inbox = models.CharField(max_length=255, unique=True)
|
||||||
shared_inbox = models.CharField(max_length=255)
|
shared_inbox = models.CharField(max_length=255)
|
||||||
|
federated_server = models.ForeignKey(
|
||||||
|
'FederatedServer',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
outbox = models.CharField(max_length=255, unique=True)
|
outbox = models.CharField(max_length=255, unique=True)
|
||||||
summary = models.TextField(blank=True, null=True)
|
summary = models.TextField(blank=True, null=True)
|
||||||
local = models.BooleanField(default=True)
|
local = models.BooleanField(default=True)
|
||||||
localname = models.CharField(
|
localname = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
# name is your display name, which you can change at will
|
# name is your display name, which you can change at will
|
||||||
name = models.CharField(max_length=100, blank=True, null=True)
|
name = models.CharField(max_length=100, blank=True, null=True)
|
||||||
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||||
# TODO: a field for if non-local users are readers or others
|
|
||||||
followers = models.ManyToManyField('self', symmetrical=False)
|
followers = models.ManyToManyField('self', symmetrical=False)
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
@ -91,6 +94,16 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
|
||||||
|
class FederatedServer(models.Model):
|
||||||
|
''' store which server's we federate with '''
|
||||||
|
server_name = models.CharField(max_length=255, unique=True)
|
||||||
|
shared_inbox = models.CharField(max_length=255, unique=True)
|
||||||
|
# federated, blocked, whatever else
|
||||||
|
status = models.CharField(max_length=255, default='federated')
|
||||||
|
# is it mastodon, fedireads, etc
|
||||||
|
application_type = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
|
|
||||||
class Activity(models.Model):
|
class Activity(models.Model):
|
||||||
''' basic fields for storing activities '''
|
''' basic fields for storing activities '''
|
||||||
uuid = models.CharField(max_length=255, unique=True)
|
uuid = models.CharField(max_length=255, unique=True)
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content">
|
<div id="content">
|
||||||
{% for result in results %}
|
{% for result in results %}
|
||||||
{{ result.username }}
|
<div>
|
||||||
|
<h2>{{ result.username }}</h2>
|
||||||
<form action="/follow/" method="post">
|
<form action="/follow/" method="post">
|
||||||
<input type="hidden" name="user" value="{{ result.id }}"></input>
|
<input type="hidden" name="user" value="{{ result.id }}"></input>
|
||||||
<input type="submit" value="Follow"></input>
|
<input type="submit" value="Follow"></input>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
''' url routing for the app and api '''
|
''' url routing for the app and api '''
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from fedireads import incoming, outgoing, views, settings
|
from fedireads import incoming, outgoing, views, settings
|
||||||
|
|
||||||
|
@ -10,27 +10,31 @@ urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
|
||||||
# federation endpoints
|
# federation endpoints
|
||||||
path('inbox', incoming.shared_inbox),
|
re_path(r'^inbox/?$', incoming.shared_inbox),
|
||||||
path('user/<str:username>.json', incoming.get_actor),
|
re_path(r'^user/(?P<username>\w+).json/?$', incoming.get_actor),
|
||||||
path('user/<str:username>/inbox', incoming.inbox),
|
re_path(r'^user/(?P<username>\w+)/inbox/?$', incoming.inbox),
|
||||||
path('user/<str:username>/outbox', outgoing.outbox),
|
re_path(r'^user/(?P<username>\w+)/outbox/?$', outgoing.outbox),
|
||||||
path('.well-known/webfinger', incoming.webfinger),
|
re_path(r'^user/(?P<username>\w+)/followers/?$', incoming.get_followers),
|
||||||
|
re_path(r'^user/(?P<username>\w+)/following/?$', incoming.get_following),
|
||||||
|
re_path(r'^.well-known/webfinger/?$', incoming.webfinger),
|
||||||
|
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
|
||||||
|
|
||||||
# ui views
|
# ui views
|
||||||
path('', views.home),
|
re_path(r'^/?$', views.home),
|
||||||
path('register/', views.register),
|
re_path(r'^register/?$', views.register),
|
||||||
path('login/', views.user_login),
|
re_path(r'^login/?$', views.user_login),
|
||||||
path('logout/', views.user_logout),
|
re_path(r'^logout/?$', views.user_logout),
|
||||||
path('user/<str:username>', views.user_profile),
|
# this endpoint is both ui and fed depending on Accept type
|
||||||
path('user/<str:username>/edit/', views.user_profile_edit),
|
re_path(r'^user/(?P<username>[\w@\.]+)/?$', views.user_profile),
|
||||||
path('work/<str:book_identifier>', views.book_page),
|
re_path(r'^user/(?P<username>\w+)/edit/?$', views.user_profile_edit),
|
||||||
|
re_path(r'^work/(?P<book_identifier>\w+)/?$', views.book_page),
|
||||||
|
|
||||||
# internal action endpoints
|
# internal action endpoints
|
||||||
path('review/', views.review),
|
re_path(r'^review/?$', views.review),
|
||||||
path('shelve/<str:shelf_id>/<int:book_id>', views.shelve),
|
re_path(r'^shelve/(?P<shelf_id>\w+)/(?P<book_id>\d+)/?$', views.shelve),
|
||||||
path('follow/', views.follow),
|
re_path(r'^follow/?$', views.follow),
|
||||||
path('unfollow/', views.unfollow),
|
re_path(r'^unfollow/?$', views.unfollow),
|
||||||
path('search/', views.search),
|
re_path(r'^search/?$', views.search),
|
||||||
path('edit_profile/', views.edit_profile),
|
re_path(r'^edit_profile/?$', views.edit_profile),
|
||||||
|
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.template.response import TemplateResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from fedireads import forms, models, openlibrary, outgoing as api
|
from fedireads import forms, models, openlibrary, outgoing, incoming
|
||||||
from fedireads.settings import DOMAIN
|
from fedireads.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,9 +107,14 @@ def register(request):
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def user_profile(request, username):
|
def user_profile(request, username):
|
||||||
''' profile page for a user '''
|
''' profile page for a user '''
|
||||||
|
content = request.headers.get('Accept')
|
||||||
|
if 'json' in content:
|
||||||
|
# we have a json request
|
||||||
|
return incoming.get_actor(request, username)
|
||||||
|
|
||||||
|
# otherwise we're at a UI view
|
||||||
try:
|
try:
|
||||||
user = models.User.objects.get(localname=username)
|
user = models.User.objects.get(localname=username)
|
||||||
except models.User.DoesNotExist:
|
except models.User.DoesNotExist:
|
||||||
|
@ -184,11 +189,14 @@ def shelve(request, shelf_id, book_id, reshelve=True):
|
||||||
desired_shelf = models.Shelf.objects.get(identifier=shelf_id)
|
desired_shelf = models.Shelf.objects.get(identifier=shelf_id)
|
||||||
if reshelve:
|
if reshelve:
|
||||||
try:
|
try:
|
||||||
current_shelf = models.Shelf.objects.get(user=request.user, book=book)
|
current_shelf = models.Shelf.objects.get(
|
||||||
api.handle_unshelve(request.user, book, current_shelf)
|
user=request.user,
|
||||||
|
book=book
|
||||||
|
)
|
||||||
|
outgoing.handle_unshelve(request.user, book, current_shelf)
|
||||||
except models.Shelf.DoesNotExist:
|
except models.Shelf.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
api.handle_shelve(request.user, book, desired_shelf)
|
outgoing.handle_shelve(request.user, book, desired_shelf)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
@ -208,7 +216,7 @@ def review(request):
|
||||||
content = form.data.get('review_content')
|
content = form.data.get('review_content')
|
||||||
rating = form.data.get('rating')
|
rating = form.data.get('rating')
|
||||||
|
|
||||||
api.handle_review(request.user, book, name, content, rating)
|
outgoing.handle_review(request.user, book, name, content, rating)
|
||||||
return redirect(book_identifier)
|
return redirect(book_identifier)
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,7 +228,7 @@ def follow(request):
|
||||||
# should this be an actor rather than an id? idk
|
# should this be an actor rather than an id? idk
|
||||||
to_follow = models.User.objects.get(id=to_follow)
|
to_follow = models.User.objects.get(id=to_follow)
|
||||||
|
|
||||||
api.handle_outgoing_follow(request.user, to_follow)
|
outgoing.handle_outgoing_follow(request.user, to_follow)
|
||||||
return redirect('/user/%s' % to_follow.username)
|
return redirect('/user/%s' % to_follow.username)
|
||||||
|
|
||||||
|
|
||||||
|
@ -241,9 +249,11 @@ def search(request):
|
||||||
''' that search bar up top '''
|
''' that search bar up top '''
|
||||||
query = request.GET.get('q')
|
query = request.GET.get('q')
|
||||||
if re.match(r'\w+@\w+.\w+', query):
|
if re.match(r'\w+@\w+.\w+', query):
|
||||||
results = [api.handle_account_search(query)]
|
# if something looks like a username, search with webfinger
|
||||||
|
results = [outgoing.handle_account_search(query)]
|
||||||
template = 'user_results.html'
|
template = 'user_results.html'
|
||||||
else:
|
else:
|
||||||
|
# just send the question over to openlibrary for book search
|
||||||
results = openlibrary.book_search(query)
|
results = openlibrary.book_search(query)
|
||||||
template = 'book_results.html'
|
template = 'book_results.html'
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue