mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-25 11:01:12 +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 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()
|
||||
|
||||
|
||||
|
|
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 '''
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
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
|
||||
|
||||
|
||||
@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 '''
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue