Browse Source

Add mark all read button to unread notifications

This will only mark notifications up to the timestamp of the most recent
one shown on the unread notifications page where it was clicked.
merge-requests/25/head
James Southern 6 years ago
committed by Deimos
parent
commit
45b2de0a44
  1. 10
      tildes/tildes/models/comment/comment_notification.py
  2. 28
      tildes/tildes/resources/comment.py
  3. 7
      tildes/tildes/routes.py
  4. 15
      tildes/tildes/templates/notifications_unread.jinja2
  5. 102
      tildes/tildes/views/api/web/comment.py

10
tildes/tildes/models/comment/comment_notification.py

@ -2,8 +2,9 @@
from datetime import datetime from datetime import datetime
import re import re
from typing import List, Tuple
from typing import Any, List, Sequence, Tuple
from pyramid.security import Allow, DENY_ALL
from sqlalchemy import Boolean, Column, ForeignKey, Integer, TIMESTAMP from sqlalchemy import Boolean, Column, ForeignKey, Integer, TIMESTAMP
from sqlalchemy.dialects.postgresql import ENUM from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy.orm import relationship, Session from sqlalchemy.orm import relationship, Session
@ -66,6 +67,13 @@ class CommentNotification(DatabaseModel):
self.comment = comment self.comment = comment
self.notification_type = notification_type self.notification_type = notification_type
def __acl__(self) -> Sequence[Tuple[str, Any, str]]:
"""Pyramid security ACL."""
acl = []
acl.append((Allow, self.user_id, 'mark_read'))
acl.append(DENY_ALL)
return acl
@property @property
def is_comment_reply(self) -> bool: def is_comment_reply(self) -> bool:
"""Return whether this is a comment reply notification.""" """Return whether this is a comment reply notification."""

28
tildes/tildes/resources/comment.py

@ -1,10 +1,11 @@
"""Root factories for comments.""" """Root factories for comments."""
from pyramid.httpexceptions import HTTPForbidden
from pyramid.request import Request from pyramid.request import Request
from webargs.pyramidparser import use_kwargs from webargs.pyramidparser import use_kwargs
from tildes.lib.id import id36_to_id from tildes.lib.id import id36_to_id
from tildes.models.comment import Comment
from tildes.models.comment import Comment, CommentNotification
from tildes.resources import get_resource from tildes.resources import get_resource
from tildes.schemas.comment import CommentSchema from tildes.schemas.comment import CommentSchema
@ -22,3 +23,28 @@ def comment_by_id36(request: Request, comment_id36: str) -> Comment:
) )
return get_resource(request, query) return get_resource(request, query)
@use_kwargs(
CommentSchema(only=('comment_id36',)),
locations=('matchdict',),
)
def notification_by_comment_id36(
request: Request,
comment_id36: str,
) -> CommentNotification:
"""Get a comment notification specified by {comment_id36} in the route.
Looks up a comment notification for the logged-in user with the
{comment_id36} specified in the route.
"""
if not request.user:
raise HTTPForbidden
comment_id = id36_to_id(comment_id36)
query = request.query(CommentNotification).filter_by(
user=request.user,
comment_id=comment_id,
)
return get_resource(request, query)

7
tildes/tildes/routes.py

@ -6,7 +6,10 @@ from pyramid.config import Configurator
from pyramid.request import Request from pyramid.request import Request
from pyramid.security import Allow, Authenticated from pyramid.security import Allow, Authenticated
from tildes.resources.comment import comment_by_id36
from tildes.resources.comment import (
comment_by_id36,
notification_by_comment_id36,
)
from tildes.resources.group import group_by_path from tildes.resources.group import group_by_path
from tildes.resources.message import message_conversation_by_id36 from tildes.resources.message import message_conversation_by_id36
from tildes.resources.topic import topic_by_id36 from tildes.resources.topic import topic_by_id36
@ -158,7 +161,7 @@ def add_intercooler_routes(config: Configurator) -> None:
add_ic_route( add_ic_route(
'comment_mark_read', 'comment_mark_read',
'/comments/{comment_id36}/mark_read', '/comments/{comment_id36}/mark_read',
factory=comment_by_id36,
factory=notification_by_comment_id36,
) )
add_ic_route( add_ic_route(

15
tildes/tildes/templates/notifications_unread.jinja2

@ -5,7 +5,20 @@
{% block title %}Unread notifications{% endblock %} {% block title %}Unread notifications{% endblock %}
{% block main_heading %}Unread notifications{% endblock %}
{% block main_heading %}Unread notifications
{% if notifications and not request.user.auto_mark_notifications_read %}
<button
class="btn btn-link-minimal ml-2"
data-ic-put-to="{{ request.route_url(
'ic_comment_mark_read',
comment_id36=notifications[0].comment.comment_id36,
_query={"mark_all_previous": "true"},
)}}",
data-ic-target="closest main"
>Mark all read</button>
{% endif %}
{% endblock %}
{% block content %} {% block content %}
{% if notifications %} {% if notifications %}

102
tildes/tildes/views/api/web/comment.py

@ -1,9 +1,11 @@
"""Web API endpoints related to comments.""" """Web API endpoints related to comments."""
from marshmallow.fields import Boolean
from pyramid.request import Request from pyramid.request import Request
from pyramid.response import Response from pyramid.response import Response
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.exc import FlushError from sqlalchemy.orm.exc import FlushError
from webargs.pyramidparser import use_kwargs from webargs.pyramidparser import use_kwargs
from zope.sqlalchemy import mark_changed from zope.sqlalchemy import mark_changed
@ -22,6 +24,41 @@ from tildes.views import IC_NOOP
from tildes.views.decorators import ic_view_config from tildes.views.decorators import ic_view_config
def _increment_topic_comments_seen(
request: Request,
comment: Comment,
) -> None:
"""Increment the number of comments in a topic the user has viewed.
If the user has the "track comment visits" feature enabled, we want to
increment the number of comments they've seen in the thread that the
comment came from, so that they don't *both* get a notification as well as
have the thread highlight with "(1 new)". This should only happen if their
last visit was before the comment was posted, however. Below, this is
implemented as a INSERT ... ON CONFLICT DO UPDATE so that it will insert
a new topic visit with 1 comment if they didn't previously have one at
all.
"""
if request.user.track_comment_visits:
statement = (
insert(TopicVisit.__table__)
.values(
user_id=request.user.user_id,
topic_id=comment.topic_id,
visit_time=utc_now(),
num_comments=1,
)
.on_conflict_do_update(
constraint=TopicVisit.__table__.primary_key,
set_={'num_comments': TopicVisit.num_comments + 1},
where=TopicVisit.visit_time < comment.created_time,
)
)
request.db_session.execute(statement)
mark_changed(request.db_session)
@ic_view_config( @ic_view_config(
route_name='topic_comments', route_name='topic_comments',
request_method='POST', request_method='POST',
@ -287,41 +324,40 @@ def untag_comment(request: Request, name: CommentTagOption) -> Response:
request_method='PUT', request_method='PUT',
permission='mark_read', permission='mark_read',
) )
def mark_read_comment(request: Request) -> Response:
"""Mark a comment read (clear all notifications)."""
comment = request.context
request.query(CommentNotification).filter(
CommentNotification.user == request.user,
CommentNotification.comment == comment,
).update(
{CommentNotification.is_unread: False}, synchronize_session=False)
# If the user has the "track comment visits" feature enabled, we want to
# increment the number of comments they've seen in the thread that the
# comment came from, so that they don't *both* get a notification as well
# as have the thread highlight with "(1 new)". This should only happen if
# their last visit was before the comment was posted, however.
# Below, this is implemented as a INSERT ... ON CONFLICT DO UPDATE so that
# it will insert a new topic visit with 1 comment if they didn't previously
# have one at all.
if request.user.track_comment_visits:
statement = (
insert(TopicVisit.__table__)
.values(
user_id=request.user.user_id,
topic_id=comment.topic_id,
visit_time=utc_now(),
num_comments=1,
)
.on_conflict_do_update(
constraint=TopicVisit.__table__.primary_key,
set_={'num_comments': TopicVisit.num_comments + 1},
where=TopicVisit.visit_time < comment.created_time,
@use_kwargs({'mark_all_previous': Boolean(missing=False)})
def put_mark_comments_read(
request: Request,
mark_all_previous: bool,
) -> Response:
"""Mark comment(s) read, clearing notifications.
The "main" notification (request.context) will always be marked read, and
if the query param mark_all_previous is Truthy, all notifications prior to
that one will be marked read as well.
"""
notification = request.context
if mark_all_previous:
prev_notifications = (
request.query(CommentNotification).filter(
CommentNotification.user == request.user,
CommentNotification.is_unread == True, # noqa
CommentNotification.created_time <= notification.created_time,
) )
.options(joinedload(CommentNotification.comment))
.all()
) )
request.db_session.execute(statement)
mark_changed(request.db_session)
for comment_notification in prev_notifications:
comment_notification.is_unread = False
_increment_topic_comments_seen(
request,
comment_notification.comment
)
return Response('Your comment notifications have been cleared.')
notification.is_unread = False
_increment_topic_comments_seen(request, notification.comment)
return IC_NOOP return IC_NOOP
Loading…
Cancel
Save