mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-25 19:11:09 +00:00
parent
95c8dc1d67
commit
f4008eb8c8
13 changed files with 176 additions and 5 deletions
|
@ -13,7 +13,8 @@ import requests
|
||||||
from fedireads import activitypub
|
from fedireads import activitypub
|
||||||
from fedireads import models
|
from fedireads import models
|
||||||
from fedireads import outgoing
|
from fedireads import outgoing
|
||||||
from fedireads.status import create_review, create_status, create_tag
|
from fedireads.status import create_review, create_status, create_tag, \
|
||||||
|
create_notification
|
||||||
from fedireads.remote_user import get_or_create_remote_user
|
from fedireads.remote_user import get_or_create_remote_user
|
||||||
|
|
||||||
|
|
||||||
|
@ -212,6 +213,7 @@ def handle_incoming_follow(activity):
|
||||||
# Accept, but then do we need to match the activity id?
|
# Accept, but then do we need to match the activity id?
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
create_notification(to_follow, 'FOLLOW', related_user=user)
|
||||||
outgoing.handle_outgoing_accept(user, to_follow, activity)
|
outgoing.handle_outgoing_accept(user, to_follow, activity)
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
@ -271,7 +273,14 @@ def handle_incoming_create(activity):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
elif not user.local:
|
elif not user.local:
|
||||||
try:
|
try:
|
||||||
create_status(user, content)
|
status = create_status(user, content)
|
||||||
|
if status.reply_parent:
|
||||||
|
create_notification(
|
||||||
|
status.reply_parent.user,
|
||||||
|
'REPLY',
|
||||||
|
related_user=status.user,
|
||||||
|
related_status=status,
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
@ -289,6 +298,13 @@ def handle_incoming_favorite(activity):
|
||||||
|
|
||||||
if not liker.local:
|
if not liker.local:
|
||||||
status.favorites.add(liker)
|
status.favorites.add(liker)
|
||||||
|
|
||||||
|
create_notification(
|
||||||
|
status.user,
|
||||||
|
'FAVORITE',
|
||||||
|
related_user=liker,
|
||||||
|
related_status=status,
|
||||||
|
)
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
|
|
32
fedireads/migrations/0011_notification.py
Normal file
32
fedireads/migrations/0011_notification.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-03-07 22:23
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fedireads', '0010_auto_20200307_0655'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Notification',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
|
('read', models.BooleanField(default=False)),
|
||||||
|
('notification_type', models.CharField(max_length=255)),
|
||||||
|
('related_book', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
||||||
|
('related_status', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Status')),
|
||||||
|
('related_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='related_user', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,5 @@
|
||||||
''' bring all the models into the app namespace '''
|
''' bring all the models into the app namespace '''
|
||||||
from .book import Book, Work, Edition, Author
|
from .book import Book, Work, Edition, Author
|
||||||
from .shelf import Shelf, ShelfBook
|
from .shelf import Shelf, ShelfBook
|
||||||
from .status import Status, Review, Favorite, Tag
|
from .status import Status, Review, Favorite, Tag, Notification
|
||||||
from .user import User, UserRelationship, FederatedServer
|
from .user import User, UserRelationship, FederatedServer
|
||||||
|
|
|
@ -76,3 +76,29 @@ class Tag(FedireadsModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('user', 'book', 'name')
|
unique_together = ('user', 'book', 'name')
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(FedireadsModel):
|
||||||
|
''' you've been tagged, liked, followed, etc '''
|
||||||
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||||
|
related_book = models.ForeignKey(
|
||||||
|
'Book', on_delete=models.PROTECT, null=True)
|
||||||
|
related_user = models.ForeignKey(
|
||||||
|
'User',
|
||||||
|
on_delete=models.PROTECT, null=True, related_name='related_user')
|
||||||
|
related_status = models.ForeignKey(
|
||||||
|
'Status', on_delete=models.PROTECT, null=True)
|
||||||
|
read = models.BooleanField(default=False)
|
||||||
|
notification_type = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# TODO: there's probably a real way to do enums
|
||||||
|
types = [
|
||||||
|
'FAVORITE',
|
||||||
|
'REPLY',
|
||||||
|
'TAG',
|
||||||
|
'FOLLOW'
|
||||||
|
]
|
||||||
|
if not self.notification_type in types:
|
||||||
|
raise ValueError('Invalid notitication type')
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ from urllib.parse import urlencode
|
||||||
|
|
||||||
from fedireads import activitypub
|
from fedireads import activitypub
|
||||||
from fedireads import models
|
from fedireads import models
|
||||||
from fedireads.status import create_review, create_status, create_tag
|
from fedireads.status import create_review, create_status, create_tag, \
|
||||||
|
create_notification
|
||||||
from fedireads.remote_user import get_or_create_remote_user
|
from fedireads.remote_user import get_or_create_remote_user
|
||||||
from fedireads.broadcast import get_recipients, broadcast
|
from fedireads.broadcast import get_recipients, broadcast
|
||||||
|
|
||||||
|
@ -189,6 +190,13 @@ def handle_comment(user, review, content):
|
||||||
''' respond to a review or status '''
|
''' respond to a review or status '''
|
||||||
# validated and saves the comment in the database so it has an id
|
# validated and saves the comment in the database so it has an id
|
||||||
comment = create_status(user, content, reply_parent=review)
|
comment = create_status(user, content, reply_parent=review)
|
||||||
|
if comment.reply_parent:
|
||||||
|
create_notification(
|
||||||
|
comment.reply_parent.user,
|
||||||
|
'REPLY',
|
||||||
|
related_user=user,
|
||||||
|
related_status=comment,
|
||||||
|
)
|
||||||
comment_activity = activitypub.get_status(comment)
|
comment_activity = activitypub.get_status(comment)
|
||||||
create_activity = activitypub.get_create(user, comment_activity)
|
create_activity = activitypub.get_create(user, comment_activity)
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,18 @@ def create_tag(user, possible_book, name):
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
|
||||||
|
def create_notification(user, notification_type, related_user=None, \
|
||||||
|
related_book=None, related_status=None):
|
||||||
|
''' let a user know when someone interacts with their content '''
|
||||||
|
models.Notification.objects.create(
|
||||||
|
user=user,
|
||||||
|
related_book=related_book,
|
||||||
|
related_user=related_user,
|
||||||
|
related_status=related_status,
|
||||||
|
notification_type=notification_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def sanitize(content):
|
def sanitize(content):
|
||||||
''' remove invalid html from free text '''
|
''' remove invalid html from free text '''
|
||||||
parser = InputHtmlParser()
|
parser = InputHtmlParser()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{% load fr_display %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -47,6 +48,13 @@
|
||||||
<input type="submit" value="🔍"></input>
|
<input type="submit" value="🔍"></input>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<div id="notification">
|
||||||
|
<a href="/notifications">
|
||||||
|
🔔 ({{ request.user | notification_count }})
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
37
fedireads/templates/notifications.html
Normal file
37
fedireads/templates/notifications.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load humanize %}l
|
||||||
|
{% block content %}
|
||||||
|
<div id="content">
|
||||||
|
<div>
|
||||||
|
<h2>Notifications</h2>
|
||||||
|
<form name="clear" action="/clear-notifications" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Delete notifications</button>
|
||||||
|
</form>
|
||||||
|
{% for notification in notifications %}
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{% if notification.notification_type == 'FAVORITE' %}
|
||||||
|
{% include 'snippets/username.html' with user=notification.related_user %}
|
||||||
|
favorited your
|
||||||
|
<a href="{{ notification.related_status.absolute_id}}">status</a>
|
||||||
|
|
||||||
|
{% elif notification.notification_type == 'REPLY' %}
|
||||||
|
{% include 'snippets/username.html' with user=notification.related_user %}
|
||||||
|
<a href="{{ notification.related_status.absolute_id}}">replied</a>
|
||||||
|
to your
|
||||||
|
<a href="{{ notification.related_status.reply_parent.absolute_id}}">status</a>
|
||||||
|
|
||||||
|
{% elif notification.notification_type == 'FOLLOW' %}
|
||||||
|
{% include 'snippets/username.html' with user=notification.related_user %}
|
||||||
|
followed you
|
||||||
|
{% endif %}
|
||||||
|
<small>{{ notification.created_date | naturaltime }}</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
11
fedireads/templates/snippets/user_preview.html
Normal file
11
fedireads/templates/snippets/user_preview.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{% include 'snippets/avatar.html' with user=user %}
|
||||||
|
{% include 'snippets/username.html' with user=user %}
|
||||||
|
<small>{{ user.username }}</small>
|
||||||
|
</div>
|
||||||
|
{% if not is_self %}
|
||||||
|
{% include 'snippets/follow_button.html' with user=user %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
|
@ -49,6 +49,12 @@ def get_user_identifier(user):
|
||||||
return user.localname if user.localname else user.username
|
return user.localname if user.localname else user.username
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='notification_count')
|
||||||
|
def get_notification_count(user):
|
||||||
|
''' how many UNREAD notifications are there '''
|
||||||
|
return user.notification_set.filter(read=False).count()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def shelve_button_identifier(context, book):
|
def shelve_button_identifier(context, book):
|
||||||
''' check what shelf a user has a book on, if any '''
|
''' check what shelf a user has a book on, if any '''
|
||||||
|
|
|
@ -38,7 +38,7 @@ urlpatterns = [
|
||||||
re_path(r'^register/?$', views.register),
|
re_path(r'^register/?$', views.register),
|
||||||
re_path(r'^login/?$', views.user_login),
|
re_path(r'^login/?$', views.user_login),
|
||||||
re_path(r'^logout/?$', views.user_logout),
|
re_path(r'^logout/?$', views.user_logout),
|
||||||
# this endpoint is both ui and fed depending on Accept type
|
re_path(r'^notifications/?', views.notifications_page),
|
||||||
re_path(r'%s/?$' % user_path, views.user_page),
|
re_path(r'%s/?$' % user_path, views.user_page),
|
||||||
re_path(r'%s/edit/?$' % user_path, views.edit_profile_page),
|
re_path(r'%s/edit/?$' % user_path, views.edit_profile_page),
|
||||||
re_path(r'^user/edit/?$', views.edit_profile_page),
|
re_path(r'^user/edit/?$', views.edit_profile_page),
|
||||||
|
@ -59,5 +59,6 @@ urlpatterns = [
|
||||||
re_path(r'^unfollow/?$', actions.unfollow),
|
re_path(r'^unfollow/?$', actions.unfollow),
|
||||||
re_path(r'^search/?$', actions.search),
|
re_path(r'^search/?$', actions.search),
|
||||||
re_path(r'^edit_profile/?$', actions.edit_profile),
|
re_path(r'^edit_profile/?$', actions.edit_profile),
|
||||||
|
re_path(r'^clear-notifications/?$', actions.clear_notifications),
|
||||||
|
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
@ -157,3 +157,8 @@ def search(request):
|
||||||
return TemplateResponse(request, template, {'results': results})
|
return TemplateResponse(request, template, {'results': results})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def clear_notifications(request):
|
||||||
|
request.user.notification_set.filter(read=True).delete()
|
||||||
|
return redirect('/notifications')
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,15 @@ def register(request):
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
def notifications_page(request):
|
||||||
|
''' list notitications '''
|
||||||
|
data = {
|
||||||
|
'notifications': request.user.notification_set.all().order_by('-created_date')
|
||||||
|
}
|
||||||
|
request.user.notification_set.update(read=True)
|
||||||
|
return TemplateResponse(request, 'notifications.html', data)
|
||||||
|
|
||||||
|
|
||||||
def user_page(request, username):
|
def user_page(request, username):
|
||||||
''' profile page for a user '''
|
''' profile page for a user '''
|
||||||
content = request.headers.get('Accept')
|
content = request.headers.get('Accept')
|
||||||
|
|
Loading…
Reference in a new issue