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 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()

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

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

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

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

View file

@ -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)

View file

@ -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')

View file

@ -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')