mirror of https://gitlab.com/tildes/tildes.git
Browse Source
Add bookmarking for topics and comments
Add bookmarking for topics and comments
Allows users to "bookmark" topics and comments, and view their list of bookmarks through their user page.merge-requests/51/head
Ivan Fonseca
6 years ago
committed by
Deimos
19 changed files with 511 additions and 12 deletions
-
87tildes/alembic/versions/53567981cdf4_add_topic_and_comment_bookmark_tables.py
-
2tildes/tildes/__init__.py
-
3tildes/tildes/database_models.py
-
1tildes/tildes/models/comment/__init__.py
-
4tildes/tildes/models/comment/comment.py
-
38tildes/tildes/models/comment/comment_bookmark.py
-
21tildes/tildes/models/comment/comment_query.py
-
1tildes/tildes/models/topic/__init__.py
-
4tildes/tildes/models/topic/topic.py
-
38tildes/tildes/models/topic/topic_bookmark.py
-
19tildes/tildes/models/topic/topic_query.py
-
8tildes/tildes/routes.py
-
59tildes/tildes/templates/bookmarks.jinja2
-
23tildes/tildes/templates/macros/comments.jinja2
-
5tildes/tildes/templates/macros/user_menu.jinja2
-
24tildes/tildes/templates/topic.jinja2
-
63tildes/tildes/views/api/web/comment.py
-
40tildes/tildes/views/api/web/topic.py
-
69tildes/tildes/views/bookmarks.py
@ -0,0 +1,87 @@ |
|||||
|
"""Add topic and comment bookmark tables |
||||
|
|
||||
|
Revision ID: 53567981cdf4 |
||||
|
Revises: 5a7dc1032efc |
||||
|
Create Date: 2018-08-17 17:57:22.171858 |
||||
|
|
||||
|
""" |
||||
|
from alembic import op |
||||
|
import sqlalchemy as sa |
||||
|
|
||||
|
|
||||
|
# revision identifiers, used by Alembic. |
||||
|
revision = "53567981cdf4" |
||||
|
down_revision = "5a7dc1032efc" |
||||
|
branch_labels = None |
||||
|
depends_on = None |
||||
|
|
||||
|
|
||||
|
def upgrade(): |
||||
|
op.create_table( |
||||
|
"topic_bookmarks", |
||||
|
sa.Column("user_id", sa.Integer(), nullable=False), |
||||
|
sa.Column("topic_id", sa.Integer(), nullable=False), |
||||
|
sa.Column( |
||||
|
"created_time", |
||||
|
sa.TIMESTAMP(timezone=True), |
||||
|
server_default=sa.text("NOW()"), |
||||
|
nullable=False, |
||||
|
), |
||||
|
sa.ForeignKeyConstraint( |
||||
|
["topic_id"], |
||||
|
["topics.topic_id"], |
||||
|
name=op.f("fk_topic_bookmarks_topic_id_topics"), |
||||
|
), |
||||
|
sa.ForeignKeyConstraint( |
||||
|
["user_id"], |
||||
|
["users.user_id"], |
||||
|
name=op.f("fk_topic_bookmarks_user_id_users"), |
||||
|
), |
||||
|
sa.PrimaryKeyConstraint("user_id", "topic_id", name=op.f("pk_topic_bookmarks")), |
||||
|
) |
||||
|
op.create_index( |
||||
|
op.f("ix_topic_bookmarks_created_time"), |
||||
|
"topic_bookmarks", |
||||
|
["created_time"], |
||||
|
unique=False, |
||||
|
) |
||||
|
|
||||
|
op.create_table( |
||||
|
"comment_bookmarks", |
||||
|
sa.Column("user_id", sa.Integer(), nullable=False), |
||||
|
sa.Column("comment_id", sa.Integer(), nullable=False), |
||||
|
sa.Column( |
||||
|
"created_time", |
||||
|
sa.TIMESTAMP(timezone=True), |
||||
|
server_default=sa.text("NOW()"), |
||||
|
nullable=False, |
||||
|
), |
||||
|
sa.ForeignKeyConstraint( |
||||
|
["comment_id"], |
||||
|
["comments.comment_id"], |
||||
|
name=op.f("fk_comment_bookmarks_comment_id_comments"), |
||||
|
), |
||||
|
sa.ForeignKeyConstraint( |
||||
|
["user_id"], |
||||
|
["users.user_id"], |
||||
|
name=op.f("fk_comment_bookmarks_user_id_users"), |
||||
|
), |
||||
|
sa.PrimaryKeyConstraint( |
||||
|
"user_id", "comment_id", name=op.f("pk_comment_bookmarks") |
||||
|
), |
||||
|
) |
||||
|
op.create_index( |
||||
|
op.f("ix_comment_bookmarks_created_time"), |
||||
|
"comment_bookmarks", |
||||
|
["created_time"], |
||||
|
unique=False, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def downgrade(): |
||||
|
op.drop_index( |
||||
|
op.f("ix_comment_bookmarks_created_time"), table_name="comment_bookmarks" |
||||
|
) |
||||
|
op.drop_table("comment_bookmarks") |
||||
|
op.drop_index(op.f("ix_topic_bookmarks_created_time"), table_name="topic_bookmarks") |
||||
|
op.drop_table("topic_bookmarks") |
@ -0,0 +1,38 @@ |
|||||
|
"""Contains the CommentBookmark class.""" |
||||
|
|
||||
|
from datetime import datetime |
||||
|
|
||||
|
from sqlalchemy import Column, ForeignKey, Integer, TIMESTAMP |
||||
|
from sqlalchemy.orm import relationship |
||||
|
from sqlalchemy.sql.expression import text |
||||
|
|
||||
|
from tildes.models import DatabaseModel |
||||
|
from tildes.models.comment import Comment |
||||
|
from tildes.models.user import User |
||||
|
|
||||
|
|
||||
|
class CommentBookmark(DatabaseModel): |
||||
|
"""Model for a comment bookmark.""" |
||||
|
|
||||
|
__tablename__ = "comment_bookmarks" |
||||
|
|
||||
|
user_id: int = Column( |
||||
|
Integer, ForeignKey("users.user_id"), nullable=False, primary_key=True |
||||
|
) |
||||
|
comment_id: int = Column( |
||||
|
Integer, ForeignKey("comments.comment_id"), nullable=False, primary_key=True |
||||
|
) |
||||
|
created_time: datetime = Column( |
||||
|
TIMESTAMP(timezone=True), |
||||
|
nullable=False, |
||||
|
index=True, |
||||
|
server_default=text("NOW()"), |
||||
|
) |
||||
|
|
||||
|
user: User = relationship("User", innerjoin=True) |
||||
|
comment: Comment = relationship("Comment", innerjoin=True) |
||||
|
|
||||
|
def __init__(self, user: User, comment: Comment) -> None: |
||||
|
"""Create a new comment bookmark.""" |
||||
|
self.user = user |
||||
|
self.comment = comment |
@ -1,6 +1,7 @@ |
|||||
"""Contains models related to topics.""" |
"""Contains models related to topics.""" |
||||
|
|
||||
from .topic import EDIT_GRACE_PERIOD, Topic |
from .topic import EDIT_GRACE_PERIOD, Topic |
||||
|
from .topic_bookmark import TopicBookmark |
||||
from .topic_query import TopicQuery |
from .topic_query import TopicQuery |
||||
from .topic_visit import TopicVisit |
from .topic_visit import TopicVisit |
||||
from .topic_vote import TopicVote |
from .topic_vote import TopicVote |
@ -0,0 +1,38 @@ |
|||||
|
"""Contains the TopicBookmark class.""" |
||||
|
|
||||
|
from datetime import datetime |
||||
|
|
||||
|
from sqlalchemy import Column, ForeignKey, Integer, TIMESTAMP |
||||
|
from sqlalchemy.orm import relationship |
||||
|
from sqlalchemy.sql.expression import text |
||||
|
|
||||
|
from tildes.models import DatabaseModel |
||||
|
from tildes.models.topic import Topic |
||||
|
from tildes.models.user import User |
||||
|
|
||||
|
|
||||
|
class TopicBookmark(DatabaseModel): |
||||
|
"""Model for a topic bookmark.""" |
||||
|
|
||||
|
__tablename__ = "topic_bookmarks" |
||||
|
|
||||
|
user_id: int = Column( |
||||
|
Integer, ForeignKey("users.user_id"), nullable=False, primary_key=True |
||||
|
) |
||||
|
topic_id: int = Column( |
||||
|
Integer, ForeignKey("topics.topic_id"), nullable=False, primary_key=True |
||||
|
) |
||||
|
created_time: datetime = Column( |
||||
|
TIMESTAMP(timezone=True), |
||||
|
nullable=False, |
||||
|
index=True, |
||||
|
server_default=text("NOW()"), |
||||
|
) |
||||
|
|
||||
|
user: User = relationship("User", innerjoin=True) |
||||
|
topic: Topic = relationship("Topic", innerjoin=True) |
||||
|
|
||||
|
def __init__(self, user: User, topic: Topic) -> None: |
||||
|
"""Create a new topic bookmark.""" |
||||
|
self.user = user |
||||
|
self.topic = topic |
@ -0,0 +1,59 @@ |
|||||
|
{% extends 'base_user_menu.jinja2' %} |
||||
|
|
||||
|
{% from 'macros/comments.jinja2' import render_single_comment with context %} |
||||
|
{% from 'macros/links.jinja2' import group_linked, username_linked %} |
||||
|
{% from 'macros/topics.jinja2' import render_topic_for_listing with context %} |
||||
|
|
||||
|
{% block title %}Bookmarks{% endblock %} |
||||
|
|
||||
|
{% block main_heading %}Bookmarks{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
|
||||
|
<div class="listing-options"> |
||||
|
<menu class="tab tab-listing-order"> |
||||
|
<li class="tab-item{{' active' if (post_type == 'topic' or not post_type) else ''}}"> |
||||
|
<a href="{{ request.current_listing_normal_url({'type': 'topic'}) }}">Topics</a> |
||||
|
</li> |
||||
|
<li class="tab-item{{ ' active' if post_type == 'comment' else ''}}"> |
||||
|
<a href="{{ request.current_listing_normal_url({'type': 'comment'}) }}">Comments</a> |
||||
|
</li> |
||||
|
</menu> |
||||
|
</div> |
||||
|
|
||||
|
{% if posts %} |
||||
|
<ol class="post-listing"> |
||||
|
{% for post in posts if request.has_permission('view', post) %} |
||||
|
<li> |
||||
|
{% if post is topic %} |
||||
|
{{ render_topic_for_listing(post, show_group=True) }} |
||||
|
{% elif post is comment %} |
||||
|
<h2>Comment on <a href="{{ post.topic.permalink }}">{{ post.topic.title }}</a> in {{ group_linked(post.topic.group.path) }}</h2> |
||||
|
{{ render_single_comment(post) }} |
||||
|
{% endif %} |
||||
|
</li> |
||||
|
{% endfor %} |
||||
|
</ol> |
||||
|
|
||||
|
{% if post_type and (posts.has_prev_page or posts.has_next_page) %} |
||||
|
<div class="pagination"> |
||||
|
{% if posts.has_prev_page %} |
||||
|
<a class="page-item btn" id="prev-page" |
||||
|
href="{{ request.current_listing_base_url({'before': posts.prev_page_before_id36}) }}" |
||||
|
>Prev</a> |
||||
|
{% endif %} |
||||
|
|
||||
|
{% if posts.has_next_page %} |
||||
|
<a class="page-item btn" id="next-page" |
||||
|
href="{{ request.current_listing_base_url({'after': posts.next_page_after_id36}) }}" |
||||
|
>Next</a> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
{% else %} |
||||
|
<div class="empty"> |
||||
|
<h2 class="empty-title">You haven't bookmarked any posts</h2> |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
|
||||
|
{% endblock %} |
@ -0,0 +1,69 @@ |
|||||
|
"""Views relating to bookmarks.""" |
||||
|
|
||||
|
from typing import Optional, Type, Union |
||||
|
|
||||
|
from marshmallow.fields import String |
||||
|
from marshmallow.validate import OneOf |
||||
|
from pyramid.request import Request |
||||
|
from pyramid.view import view_config |
||||
|
from sqlalchemy.sql import exists, desc |
||||
|
from sqlalchemy.sql.expression import and_ |
||||
|
from webargs.pyramidparser import use_kwargs |
||||
|
|
||||
|
from tildes.models.comment import Comment, CommentBookmark |
||||
|
from tildes.models.topic import Topic, TopicBookmark |
||||
|
from tildes.schemas.listing import PaginatedListingSchema |
||||
|
|
||||
|
|
||||
|
@view_config(route_name="bookmarks", renderer="bookmarks.jinja2") |
||||
|
@use_kwargs(PaginatedListingSchema) |
||||
|
@use_kwargs( |
||||
|
{"post_type": String(load_from="type", validate=OneOf(("topic", "comment")))} |
||||
|
) |
||||
|
def get_bookmarks( |
||||
|
request: Request, |
||||
|
after: str, |
||||
|
before: str, |
||||
|
per_page: int, |
||||
|
post_type: Optional[str] = None, |
||||
|
) -> dict: |
||||
|
"""Generate the bookmarks page.""" |
||||
|
user = request.user |
||||
|
|
||||
|
bookmark_cls: Union[Type[CommentBookmark], Type[TopicBookmark]] |
||||
|
|
||||
|
if post_type == "comment": |
||||
|
post_cls = Comment |
||||
|
bookmark_cls = CommentBookmark |
||||
|
else: |
||||
|
post_cls = Topic |
||||
|
bookmark_cls = TopicBookmark |
||||
|
|
||||
|
query = ( |
||||
|
request.query(post_cls) |
||||
|
.filter( |
||||
|
exists() |
||||
|
.where( |
||||
|
and_( |
||||
|
bookmark_cls.user == user, |
||||
|
bookmark_cls.topic_id == post_cls.topic_id |
||||
|
if post_cls == Topic |
||||
|
else bookmark_cls.comment_id == post_cls.comment_id, |
||||
|
) |
||||
|
) |
||||
|
.correlate(bookmark_cls) |
||||
|
) |
||||
|
.order_by(desc(bookmark_cls.created_time)) |
||||
|
) |
||||
|
|
||||
|
if before: |
||||
|
query = query.before_id36(before) |
||||
|
|
||||
|
if after: |
||||
|
query = query.after_id36(after) |
||||
|
|
||||
|
query = query.join_all_relationships() |
||||
|
|
||||
|
posts = query.get_page(per_page) |
||||
|
|
||||
|
return {"user": user, "posts": posts, "post_type": post_type} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue