mirror of https://gitlab.com/tildes/tildes.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
12 KiB
441 lines
12 KiB
# Copyright (c) 2018 Tildes contributors <code@tildes.net>
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""Web API endpoints related to topics."""
|
|
|
|
from marshmallow import ValidationError
|
|
from marshmallow.fields import String
|
|
from pyramid.httpexceptions import HTTPNotFound
|
|
from pyramid.response import Response
|
|
from pyramid.request import Request
|
|
from sqlalchemy import cast, Text
|
|
from sqlalchemy.dialects.postgresql import ARRAY
|
|
from sqlalchemy.exc import IntegrityError
|
|
from webargs.pyramidparser import use_kwargs
|
|
|
|
from tildes.enums import LogEventType
|
|
from tildes.lib.link_metadata import METADATA_KEYS
|
|
from tildes.models.group import Group
|
|
from tildes.models.log import LogTopic
|
|
from tildes.models.topic import Topic, TopicBookmark, TopicVote
|
|
from tildes.schemas.group import GroupSchema
|
|
from tildes.schemas.topic import TopicSchema
|
|
from tildes.views import IC_NOOP
|
|
from tildes.views.decorators import ic_view_config
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic",
|
|
request_method="GET",
|
|
request_param="ic-trigger-name=edit",
|
|
renderer="topic_edit.jinja2",
|
|
permission="edit",
|
|
)
|
|
def get_topic_edit(request: Request) -> dict:
|
|
"""Get the edit form for a topic with Intercooler."""
|
|
return {"topic": request.context}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic",
|
|
request_method="GET",
|
|
renderer="topic_contents.jinja2",
|
|
permission="view",
|
|
)
|
|
def get_topic_contents(request: Request) -> dict:
|
|
"""Get a topic's body with Intercooler."""
|
|
return {"topic": request.context}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic",
|
|
request_method="PATCH",
|
|
renderer="topic_contents.jinja2",
|
|
permission="edit",
|
|
)
|
|
@use_kwargs(TopicSchema(only=("markdown",)))
|
|
def patch_topic(request: Request, markdown: str) -> dict:
|
|
"""Update a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
topic.markdown = markdown
|
|
|
|
return {"topic": topic}
|
|
|
|
|
|
@ic_view_config(route_name="topic", request_method="DELETE", permission="delete")
|
|
def delete_topic(request: Request) -> Response:
|
|
"""Delete a topic with Intercooler and redirect to its group."""
|
|
topic = request.context
|
|
topic.is_deleted = True
|
|
|
|
response = Response()
|
|
response.headers["X-IC-Redirect"] = request.route_url(
|
|
"group", group_path=topic.group.path
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_vote",
|
|
request_method="PUT",
|
|
renderer="topic_voting.jinja2",
|
|
permission="vote",
|
|
)
|
|
def put_topic_vote(request: Request) -> Response:
|
|
"""Vote on a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
savepoint = request.tm.savepoint()
|
|
|
|
new_vote = TopicVote(request.user, topic)
|
|
request.db_session.add(new_vote)
|
|
|
|
request.db_session.add(LogTopic(LogEventType.TOPIC_VOTE, request, topic))
|
|
|
|
try:
|
|
# manually flush before attempting to commit, to avoid having all objects
|
|
# detached from the session in case of an error
|
|
request.db_session.flush()
|
|
request.tm.commit()
|
|
except IntegrityError:
|
|
# the user has already voted on this topic
|
|
savepoint.rollback()
|
|
|
|
# re-query the topic to get complete data
|
|
topic = (
|
|
request.query(Topic)
|
|
.join_all_relationships()
|
|
.filter_by(topic_id=topic.topic_id)
|
|
.one()
|
|
)
|
|
|
|
return {"topic": topic}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_vote",
|
|
request_method="DELETE",
|
|
renderer="topic_voting.jinja2",
|
|
permission="vote",
|
|
)
|
|
def delete_topic_vote(request: Request) -> Response:
|
|
"""Remove the user's vote from a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
request.query(TopicVote).filter(
|
|
TopicVote.topic == topic, TopicVote.user == request.user
|
|
).delete(synchronize_session=False)
|
|
|
|
request.db_session.add(LogTopic(LogEventType.TOPIC_UNVOTE, request, topic))
|
|
|
|
# manually commit the transaction so triggers will execute
|
|
request.tm.commit()
|
|
|
|
# re-query the topic to get complete data
|
|
topic = (
|
|
request.query(Topic)
|
|
.join_all_relationships()
|
|
.filter_by(topic_id=topic.topic_id)
|
|
.one()
|
|
)
|
|
|
|
return {"topic": topic}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_tags",
|
|
request_method="GET",
|
|
renderer="topic_tags_edit.jinja2",
|
|
permission="tag",
|
|
)
|
|
def get_topic_tags(request: Request) -> dict:
|
|
"""Get the tagging form for a topic with Intercooler."""
|
|
return {"topic": request.context}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_tags",
|
|
request_method="PUT",
|
|
renderer="topic_tags.jinja2",
|
|
permission="tag",
|
|
)
|
|
@use_kwargs({"tags": String()})
|
|
def put_tag_topic(request: Request, tags: str) -> dict:
|
|
"""Apply tags to a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
if tags:
|
|
# split the tag string on commas
|
|
new_tags = tags.split(",")
|
|
else:
|
|
new_tags = []
|
|
|
|
old_tags = topic.tags
|
|
|
|
try:
|
|
topic.tags = new_tags
|
|
except ValidationError:
|
|
raise ValidationError({"tags": ["Invalid tags"]})
|
|
|
|
# if tags weren't changed, don't add a log entry or update page
|
|
if set(topic.tags) == set(old_tags):
|
|
return IC_NOOP
|
|
|
|
request.db_session.add(
|
|
LogTopic(
|
|
LogEventType.TOPIC_TAG,
|
|
request,
|
|
topic,
|
|
info={"old": old_tags, "new": topic.tags},
|
|
)
|
|
)
|
|
|
|
return {"topic": topic}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_group",
|
|
request_method="GET",
|
|
renderer="topic_group_edit.jinja2",
|
|
permission="move",
|
|
)
|
|
def get_topic_group(request: Request) -> dict:
|
|
"""Get the form for moving a topic with Intercooler."""
|
|
return {"topic": request.context}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic",
|
|
request_param="ic-trigger-name=topic-move",
|
|
request_method="PATCH",
|
|
permission="move",
|
|
)
|
|
@use_kwargs(GroupSchema(only=("path",)))
|
|
def patch_move_topic(request: Request, path: str) -> dict:
|
|
"""Move a topic to a different group with Intercooler."""
|
|
topic = request.context
|
|
|
|
new_group = request.query(Group).filter(Group.path == path).one_or_none()
|
|
if not new_group:
|
|
raise HTTPNotFound("Group not found")
|
|
|
|
old_group = topic.group
|
|
|
|
if new_group == old_group:
|
|
return IC_NOOP
|
|
|
|
topic.group = new_group
|
|
|
|
request.db_session.add(
|
|
LogTopic(
|
|
LogEventType.TOPIC_MOVE,
|
|
request,
|
|
topic,
|
|
info={"old": str(old_group.path), "new": str(topic.group.path)},
|
|
)
|
|
)
|
|
|
|
return Response("Moved")
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_remove",
|
|
request_method="PUT",
|
|
permission="remove",
|
|
renderer="post_action_toggle_button.jinja2",
|
|
)
|
|
def put_topic_remove(request: Request) -> dict:
|
|
"""Remove a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
topic.is_removed = True
|
|
request.db_session.add(LogTopic(LogEventType.TOPIC_REMOVE, request, topic))
|
|
|
|
return {"name": "remove", "subject": topic, "is_toggled": True}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_remove",
|
|
request_method="DELETE",
|
|
permission="remove",
|
|
renderer="post_action_toggle_button.jinja2",
|
|
)
|
|
def delete_topic_remove(request: Request) -> dict:
|
|
"""Un-remove a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
topic.is_removed = False
|
|
request.db_session.add(LogTopic(LogEventType.TOPIC_UNREMOVE, request, topic))
|
|
|
|
return {"name": "remove", "subject": topic, "is_toggled": False}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_lock",
|
|
request_method="PUT",
|
|
permission="lock",
|
|
renderer="post_action_toggle_button.jinja2",
|
|
)
|
|
def put_topic_lock(request: Request) -> dict:
|
|
"""Lock a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
topic.is_locked = True
|
|
request.db_session.add(LogTopic(LogEventType.TOPIC_LOCK, request, topic))
|
|
|
|
return {"name": "lock", "subject": topic, "is_toggled": True}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_lock",
|
|
request_method="DELETE",
|
|
permission="lock",
|
|
renderer="post_action_toggle_button.jinja2",
|
|
)
|
|
def delete_topic_lock(request: Request) -> dict:
|
|
"""Unlock a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
topic.is_locked = False
|
|
request.db_session.add(LogTopic(LogEventType.TOPIC_UNLOCK, request, topic))
|
|
|
|
return {"name": "lock", "subject": topic, "is_toggled": False}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_title",
|
|
request_method="GET",
|
|
renderer="topic_title_edit.jinja2",
|
|
permission="edit_title",
|
|
)
|
|
def get_topic_title(request: Request) -> dict:
|
|
"""Get the form for editing a topic's title with Intercooler."""
|
|
return {"topic": request.context}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic",
|
|
request_param="ic-trigger-name=topic-title-edit",
|
|
request_method="PATCH",
|
|
permission="edit_title",
|
|
)
|
|
@use_kwargs(TopicSchema(only=("title",)))
|
|
def patch_topic_title(request: Request, title: str) -> dict:
|
|
"""Edit a topic's title with Intercooler."""
|
|
topic = request.context
|
|
|
|
if title == topic.title:
|
|
return IC_NOOP
|
|
|
|
request.db_session.add(
|
|
LogTopic(
|
|
LogEventType.TOPIC_TITLE_EDIT,
|
|
request,
|
|
topic,
|
|
info={"old": topic.title, "new": title},
|
|
)
|
|
)
|
|
|
|
topic.title = title
|
|
|
|
return Response(topic.title)
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_link",
|
|
request_method="GET",
|
|
renderer="topic_link_edit.jinja2",
|
|
permission="edit_link",
|
|
)
|
|
def get_topic_link(request: Request) -> dict:
|
|
"""Get the form for editing a topic's link with Intercooler."""
|
|
return {"topic": request.context}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic",
|
|
request_param="ic-trigger-name=topic-link-edit",
|
|
request_method="PATCH",
|
|
permission="edit_link",
|
|
)
|
|
@use_kwargs(TopicSchema(only=("link",)))
|
|
def patch_topic_link(request: Request, link: str) -> dict:
|
|
"""Edit a topic's link with Intercooler."""
|
|
topic = request.context
|
|
|
|
if link == topic.link:
|
|
return IC_NOOP
|
|
|
|
request.db_session.add(
|
|
LogTopic(
|
|
LogEventType.TOPIC_LINK_EDIT,
|
|
request,
|
|
topic,
|
|
info={"old": topic.link, "new": link},
|
|
)
|
|
)
|
|
|
|
# Wipe any old metadata from scrapers so we don't leave behind remnants
|
|
# (this probably really shouldn't be done here, but it's fine for now)
|
|
(
|
|
request.query(Topic)
|
|
.filter(Topic.topic_id == topic.topic_id)
|
|
.update(
|
|
{
|
|
"content_metadata": Topic.content_metadata.op("-")( # type: ignore
|
|
cast(METADATA_KEYS, ARRAY(Text))
|
|
)
|
|
},
|
|
synchronize_session=False,
|
|
)
|
|
)
|
|
|
|
topic.link = link
|
|
|
|
return Response(f'<a href="{topic.link}">{topic.link}</a>')
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_bookmark",
|
|
request_method="PUT",
|
|
permission="bookmark",
|
|
renderer="post_action_toggle_button.jinja2",
|
|
)
|
|
def put_topic_bookmark(request: Request) -> dict:
|
|
"""Bookmark a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
savepoint = request.tm.savepoint()
|
|
|
|
bookmark = TopicBookmark(request.user, topic)
|
|
request.db_session.add(bookmark)
|
|
|
|
try:
|
|
# manually flush before attempting to commit, to avoid having all
|
|
# objects detached from the session in case of an error
|
|
request.db_session.flush()
|
|
request.tm.commit()
|
|
except IntegrityError:
|
|
# the user has already bookmarked this topic
|
|
savepoint.rollback()
|
|
|
|
return {"name": "bookmark", "subject": topic, "is_toggled": True}
|
|
|
|
|
|
@ic_view_config(
|
|
route_name="topic_bookmark",
|
|
request_method="DELETE",
|
|
permission="bookmark",
|
|
renderer="post_action_toggle_button.jinja2",
|
|
)
|
|
def delete_topic_bookmark(request: Request) -> dict:
|
|
"""Unbookmark a topic with Intercooler."""
|
|
topic = request.context
|
|
|
|
request.query(TopicBookmark).filter(
|
|
TopicBookmark.user == request.user, TopicBookmark.topic == topic
|
|
).delete(synchronize_session=False)
|
|
|
|
return {"name": "bookmark", "subject": topic, "is_toggled": False}
|