Adds notifications

Fixes #70
This commit is contained in:
Mouse Reeve 2020-03-07 14:50:29 -08:00
parent 95c8dc1d67
commit f4008eb8c8
13 changed files with 176 additions and 5 deletions

View file

@ -13,7 +13,8 @@ import requests
from fedireads import activitypub
from fedireads import models
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
@ -212,6 +213,7 @@ def handle_incoming_follow(activity):
# Accept, but then do we need to match the activity id?
return HttpResponse()
create_notification(to_follow, 'FOLLOW', related_user=user)
outgoing.handle_outgoing_accept(user, to_follow, activity)
return HttpResponse()
@ -271,7 +273,14 @@ def handle_incoming_create(activity):
return HttpResponseBadRequest()
elif not user.local:
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:
return HttpResponseBadRequest()
@ -289,6 +298,13 @@ def handle_incoming_favorite(activity):
if not liker.local:
status.favorites.add(liker)
create_notification(
status.user,
'FAVORITE',
related_user=liker,
related_status=status,
)
return HttpResponse()

View 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,
},
),
]

View file

@ -1,5 +1,5 @@
''' bring all the models into the app namespace '''
from .book import Book, Work, Edition, Author
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

View file

@ -76,3 +76,29 @@ class Tag(FedireadsModel):
class Meta:
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)

View file

@ -7,7 +7,8 @@ from urllib.parse import urlencode
from fedireads import activitypub
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.broadcast import get_recipients, broadcast
@ -189,6 +190,13 @@ def handle_comment(user, review, content):
''' respond to a review or status '''
# validated and saves the comment in the database so it has an id
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)
create_activity = activitypub.get_create(user, comment_activity)

View file

@ -61,6 +61,18 @@ def create_tag(user, possible_book, name):
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):
''' remove invalid html from free text '''
parser = InputHtmlParser()

View file

@ -1,3 +1,4 @@
{% load fr_display %}
<!DOCTYPE html>
<html lang="en">
<head>
@ -47,6 +48,13 @@
<input type="submit" value="🔍"></input>
</form>
</div>
{% if request.user.is_authenticated %}
<div id="notification">
<a href="/notifications">
🔔 ({{ request.user | notification_count }})
</a>
</div>
{% endif %}
</div>
</div>
</header>

View 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 %}

View 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>

View file

@ -49,6 +49,12 @@ def get_user_identifier(user):
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)
def shelve_button_identifier(context, book):
''' check what shelf a user has a book on, if any '''

View file

@ -38,7 +38,7 @@ urlpatterns = [
re_path(r'^register/?$', views.register),
re_path(r'^login/?$', views.user_login),
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/edit/?$' % user_path, 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'^search/?$', actions.search),
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)

View file

@ -157,3 +157,8 @@ def search(request):
return TemplateResponse(request, template, {'results': results})
@login_required
def clear_notifications(request):
request.user.notification_set.filter(read=True).delete()
return redirect('/notifications')

View file

@ -141,6 +141,15 @@ def register(request):
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):
''' profile page for a user '''
content = request.headers.get('Accept')