From cd1e0b3cd2dd0faa5949a398cbb2c0f41d149644 Mon Sep 17 00:00:00 2001 From: NubWizard Date: Mon, 12 Jun 2023 18:17:31 -0500 Subject: [PATCH 1/2] Send new message config from user file --- tildes/tildes/models/user/user.py | 1 + tildes/tildes/routes.py | 20 ++++--- tildes/tildes/schemas/message.py | 6 +++ .../tildes/templates/macros/user_menu.jinja2 | 5 ++ .../templates/new_message_from_inbox.jinja2 | 41 ++++++++++++++ tildes/tildes/views/api/web/exceptions.py | 2 + tildes/tildes/views/api/web/user.py | 54 +++++++++++++++++++ tildes/tildes/views/exceptions.py | 14 +++-- 8 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 tildes/tildes/templates/new_message_from_inbox.jinja2 diff --git a/tildes/tildes/models/user/user.py b/tildes/tildes/models/user/user.py index b6221c7..0b23339 100644 --- a/tildes/tildes/models/user/user.py +++ b/tildes/tildes/models/user/user.py @@ -226,6 +226,7 @@ class User(DatabaseModel): "generate_invite", "search_posts", "view_removed_posts", + "send_message", ): acl.append((Allow, self.user_id, permission)) diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index bd8b8f4..1641a17 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -85,6 +85,8 @@ def includeme(config: Configurator) -> None: "/conversations/{conversation_id36}", factory=message_conversation_by_id36, ) + # Send new PM to user + config.add_route("new_message_from_inbox", "/new", factory=LoggedInFactory) config.add_route("settings", "/settings", factory=LoggedInFactory) with config.route_prefix_context("/settings"): @@ -189,14 +191,16 @@ def add_intercooler_routes(config: Configurator) -> None: class LoggedInFactory: - """Simple class to use as `factory` to restrict routes to logged-in users. - - This class can be used when a route should only be accessible to logged-in users but - doesn't already have another factory that would handle that by checking access to a - specific resource (such as a topic or message). - """ - - __acl__ = ((Allow, Authenticated, "view"),) + """Simple class to use as `factory` to restrict routes to logged-in users.""" + + __acl__ = [ + (Allow, Authenticated, "view"), + ( + Allow, + Authenticated, + "message", + ), # Add this line to give the 'message' permission to authenticated users + ] def __init__(self, request: Request): """Initialize - no-op, but needs to take the request as an arg.""" diff --git a/tildes/tildes/schemas/message.py b/tildes/tildes/schemas/message.py index 2f16075..2a758b8 100644 --- a/tildes/tildes/schemas/message.py +++ b/tildes/tildes/schemas/message.py @@ -29,3 +29,9 @@ class MessageReplySchema(Schema): markdown = Markdown() rendered_html = String(dump_only=True) created_time = DateTime(dump_only=True) + + +class NewMessageConversationSchema(MessageConversationSchema): + """Marshmallow schema for starting a new message conversation.""" + + recipient = String(required=True) diff --git a/tildes/tildes/templates/macros/user_menu.jinja2 b/tildes/tildes/templates/macros/user_menu.jinja2 index 5c36150..bf06b21 100644 --- a/tildes/tildes/templates/macros/user_menu.jinja2 +++ b/tildes/tildes/templates/macros/user_menu.jinja2 @@ -65,6 +65,11 @@ Sent messages +
  • Misc
  • diff --git a/tildes/tildes/templates/new_message_from_inbox.jinja2 b/tildes/tildes/templates/new_message_from_inbox.jinja2 new file mode 100644 index 0000000..c26bdd4 --- /dev/null +++ b/tildes/tildes/templates/new_message_from_inbox.jinja2 @@ -0,0 +1,41 @@ +{# Copyright (c) 2018 Tildes contributors #} +{# SPDX-License-Identifier: AGPL-3.0-or-later #} + +{% extends 'base_no_sidebar.jinja2' %} + +{% from 'macros/forms.jinja2' import markdown_textarea %} + +{% block title %}New private message conversation{% endblock %} + +{% block main_heading %} + Send a new private message +{% endblock %} + +{% block content %} +
    + + +
    + + +
    + +
    + + +
    + + {{ markdown_textarea(text=message) }} + +
    + +
    +
    +{% endblock %} diff --git a/tildes/tildes/views/api/web/exceptions.py b/tildes/tildes/views/api/web/exceptions.py index 827ba61..060150b 100644 --- a/tildes/tildes/views/api/web/exceptions.py +++ b/tildes/tildes/views/api/web/exceptions.py @@ -75,6 +75,8 @@ def error_to_text_response(request: Request) -> Response: elif isinstance(request.exception, HTTPBadRequest): if response.title == "Bad CSRF Token": response.text = "Page expired, reload and try again" + elif "Recipient user not found" in str(response): + response.text = "Username not found" else: response.text = "Unknown error" diff --git a/tildes/tildes/views/api/web/user.py b/tildes/tildes/views/api/web/user.py index a9b70f3..f287b12 100644 --- a/tildes/tildes/views/api/web/user.py +++ b/tildes/tildes/views/api/web/user.py @@ -13,6 +13,9 @@ from pyramid.httpexceptions import ( HTTPForbidden, HTTPUnauthorized, HTTPUnprocessableEntity, + HTTPNotFound, + HTTPFound, + HTTPBadRequest ) from pyramid.request import Request from pyramid.response import Response @@ -28,6 +31,13 @@ from tildes.schemas.topic import TopicSchema from tildes.schemas.user import UserSchema from tildes.views import IC_NOOP from tildes.views.decorators import ic_view_config, use_kwargs +from tildes.models.message import MessageConversation, MessageReply +from tildes.schemas.message import NewMessageConversationSchema + + + +from pyramid.view import view_config +#from pyramid.security import authenticated_userid PASSWORD_FIELD = UserSchema(only=("password",)).fields["password"] @@ -423,3 +433,47 @@ def delete_user_ban(request: Request) -> Response: request.context.is_banned = False return Response("Unbanned") + + + + + +@view_config( + route_name="new_message_from_inbox", + renderer="new_message_from_inbox.jinja2", + request_method="GET", + permission="view", +) +def get_new_message_form(request: Request) -> dict: + """Render form for entering a new private message to send.""" + return {} + + +# This is the view function for handling the form submission +@view_config( + route_name="new_message_from_inbox", request_method="POST", permission="message" +) +@use_kwargs( + NewMessageConversationSchema(only=("subject", "markdown", "recipient")), + location="form", +) +def post_new_message( + request: Request, subject: str, markdown: str, recipient: str +) -> HTTPFound: + """Start a new message conversation with a user.""" + recipient_user = ( + request.db_session.query(User).filter_by(username=recipient).one_or_none() + ) + + if recipient_user is None: + raise HTTPBadRequest("Recipient user not found") + + new_conversation = MessageConversation( + sender=request.user, + recipient=recipient_user, + subject=subject, + markdown=markdown, + ) + request.db_session.add(new_conversation) + + raise HTTPFound(request.route_url("messages_sent")) diff --git a/tildes/tildes/views/exceptions.py b/tildes/tildes/views/exceptions.py index f119f28..bdbf201 100644 --- a/tildes/tildes/views/exceptions.py +++ b/tildes/tildes/views/exceptions.py @@ -83,10 +83,11 @@ def generic_error_page(request: Request) -> dict: request.response.status_int = request.exception.status_int error = f"Error {request.exception.status_code} ({request.exception.title})" + description = "" if isinstance(request.exception, HTTPForbidden): - description = "You don't have access to this page" - if isinstance(request.exception, HTTPUnprocessableEntity) and isinstance( + description = request.exception + elif isinstance(request.exception, HTTPUnprocessableEntity) and isinstance( request.exception.__context__, ValidationError ): errors = errors_from_validationerror(request.exception.__context__) @@ -94,7 +95,14 @@ def generic_error_page(request: Request) -> dict: else: description = request.exception.explanation - return {"error": error, "description": description} + # For debugging, add the exception details + debug_info = { + "message": str(request.exception), + "type": str(type(request.exception)), + "args": request.exception.args, + } + + return {"error": error, "description": description, "debug_info": debug_info} @forbidden_view_config(xhr=False) From d49c69da1070cd02e22f9a7762058a739c4cad8e Mon Sep 17 00:00:00 2001 From: NubWizard Date: Mon, 12 Jun 2023 18:27:04 -0500 Subject: [PATCH 2/2] Cleans up the files from debugging --- tildes/tildes/routes.py | 7 ++++++- tildes/tildes/views/exceptions.py | 15 ++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index 1641a17..dd4f20f 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -191,7 +191,12 @@ def add_intercooler_routes(config: Configurator) -> None: class LoggedInFactory: - """Simple class to use as `factory` to restrict routes to logged-in users.""" + """Simple class to use as `factory` to restrict routes to logged-in users. + + This class can be used when a route should only be accessible to logged-in users but + doesn't already have another factory that would handle that by checking access to a + specific resource (such as a topic or message). + """ __acl__ = [ (Allow, Authenticated, "view"), diff --git a/tildes/tildes/views/exceptions.py b/tildes/tildes/views/exceptions.py index bdbf201..852b546 100644 --- a/tildes/tildes/views/exceptions.py +++ b/tildes/tildes/views/exceptions.py @@ -83,11 +83,10 @@ def generic_error_page(request: Request) -> dict: request.response.status_int = request.exception.status_int error = f"Error {request.exception.status_code} ({request.exception.title})" - description = "" if isinstance(request.exception, HTTPForbidden): - description = request.exception - elif isinstance(request.exception, HTTPUnprocessableEntity) and isinstance( + description = "You don't have access to this page" + if isinstance(request.exception, HTTPUnprocessableEntity) and isinstance( request.exception.__context__, ValidationError ): errors = errors_from_validationerror(request.exception.__context__) @@ -95,14 +94,7 @@ def generic_error_page(request: Request) -> dict: else: description = request.exception.explanation - # For debugging, add the exception details - debug_info = { - "message": str(request.exception), - "type": str(type(request.exception)), - "args": request.exception.args, - } - - return {"error": error, "description": description, "debug_info": debug_info} + return {"error": error, "description": description} @forbidden_view_config(xhr=False) @@ -112,3 +104,4 @@ def logged_out_forbidden(request: Request) -> HTTPFound: login_url = request.route_url("login", _query={"from_url": forbidden_path}) return HTTPFound(location=login_url) +