diff options
-rw-r--r-- | fietsboek/models/__init__.py | 1 | ||||
-rw-r--r-- | fietsboek/models/comment.py | 44 | ||||
-rw-r--r-- | fietsboek/models/track.py | 4 | ||||
-rw-r--r-- | fietsboek/models/user.py | 3 | ||||
-rw-r--r-- | fietsboek/routes.py | 1 | ||||
-rw-r--r-- | fietsboek/templates/details.jinja2 | 32 | ||||
-rw-r--r-- | fietsboek/views/detail.py | 30 |
7 files changed, 114 insertions, 1 deletions
diff --git a/fietsboek/models/__init__.py b/fietsboek/models/__init__.py index 723cca9..02d6257 100644 --- a/fietsboek/models/__init__.py +++ b/fietsboek/models/__init__.py @@ -13,6 +13,7 @@ import zope.sqlalchemy from .user import User, FriendRequest, Token # flake8: noqa from .badge import Badge # flake8: noqa from .track import Tag, Track, TrackCache, Upload # flake8: noqa +from .comment import Comment # flake8: noqa # Run ``configure_mappers`` after defining all of the models to ensure # all relationships can be setup. diff --git a/fietsboek/models/comment.py b/fietsboek/models/comment.py new file mode 100644 index 0000000..23f1871 --- /dev/null +++ b/fietsboek/models/comment.py @@ -0,0 +1,44 @@ +"""Comment model.""" +from sqlalchemy import ( + Column, + Integer, + ForeignKey, + Text, + DateTime, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class Comment(Base): + """Represents a comment on a track. + + :ivar id: Database ID. + :vartype id: int + :ivar author_id: ID of the comment's author. + :vartype author_id: int + :ivar track_id: ID of the track this comment belongs to. + :vartype track_id: int + :ivar date: Date on which the comment was made. + :vartype date: datetime.datetime + :ivar title: Title of the comment. + :vartype title: str + :ivar text: Text content of the comment. + :vartype text: str + :ivar author: Author of the comment. + :vartype author: fietsboek.model.user.User + :ivar track: Track that the comment belongs to. + :vartype track: fietsboek.model.track.Track + """ + # pylint: disable=too-few-public-methods + __tablename__ = "comments" + id = Column(Integer, primary_key=True) + author_id = Column(Integer, ForeignKey("users.id")) + track_id = Column(Integer, ForeignKey("tracks.id")) + date = Column(DateTime) + title = Column(Text) + text = Column(Text) + + author = relationship('User', back_populates='comments') + track = relationship('Track', back_populates='comments') diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py index 4ee25a0..cc5283a 100644 --- a/fietsboek/models/track.py +++ b/fietsboek/models/track.py @@ -27,7 +27,6 @@ from sqlalchemy import ( Table, ) from sqlalchemy.orm import relationship -from sqlalchemy.types import TypeDecorator from pyramid.i18n import TranslationString as _ @@ -122,6 +121,8 @@ class Track(Base): :vartype tagged_people: list[fietsboek.models.user.User] :ivar badges: Badges associated with this track. :vartype badges: list[fietsboek.models.badge.Badge] + :ivar comments: Comments left on this track. + :vartype comments: list[fietsboek.models.comment.Comment] """ __tablename__ = 'tracks' id = Column(Integer, primary_key=True) @@ -139,6 +140,7 @@ class Track(Base): back_populates='tagged_tracks') badges = relationship('Badge', secondary=track_badge_assoc, back_populates='tracks') tags = relationship('Tag', back_populates='track', cascade="all, delete-orphan") + comments = relationship('Comment', back_populates='track') # GPX files are XML files with a lot of repeated property names. Really, it # is quite inefficient to store a whole ton of GPS points in big XML diff --git a/fietsboek/models/user.py b/fietsboek/models/user.py index 0f0469f..1c6475e 100644 --- a/fietsboek/models/user.py +++ b/fietsboek/models/user.py @@ -82,6 +82,8 @@ class User(Base): :vartype uploads: list[fietsboek.models.track.Upload] :ivar tokens: List of tokens that this user can use. :vartype tokens: list[fietsboek.models.user.Token] + :ivar comments: List of comments left by this user. + :vartype comments: list[fietsboek.model.comment.Comment] """ __tablename__ = 'users' id = Column(Integer, primary_key=True) @@ -97,6 +99,7 @@ class User(Base): back_populates='tagged_people') uploads = relationship('Upload', back_populates='owner') tokens = relationship('Token', back_populates='user') + comments = relationship('Comment', back_populates='author') @classmethod def query_by_email(cls, email): diff --git a/fietsboek/routes.py b/fietsboek/routes.py index 3f03ae4..34917b6 100644 --- a/fietsboek/routes.py +++ b/fietsboek/routes.py @@ -19,6 +19,7 @@ def includeme(config): config.add_route('gpx', '/gpx/{id}.gpx') config.add_route('invalidate-share', '/track/{id}/invalidate-link') config.add_route('badge', '/badge/{id}') + config.add_route('add-comment', '/track/{track_id}/comment') config.add_route('admin', '/admin') config.add_route('admin-badge-add', '/admin/add-badge') diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2 index 2d6d55a..bf87b9f 100644 --- a/fietsboek/templates/details.jinja2 +++ b/fietsboek/templates/details.jinja2 @@ -113,5 +113,37 @@ <hr> {% endif %} <h2>{{ _("page.details.comments") }}</h2> + {% for comment in track.comments %} + <div class="card mb-3"> + <h5 class="card-header"> + {{ _("page.details.comments.author").format(comment.author.name) }} + </h5> + <div class="card-body"> + {% if comment.title %} + <h5 class="card-title">{{ comment.title }}</h5> + {% endif %} + {{ comment_md_to_html(comment.text) }} + </div> + <div class="card-footer text-muted"> + {{ comment.date }} + </div> + </div> + {% endfor %} + {% if request.identity %} + <div class="card"> + <form action="{{ request.route_url('add-comment', track_id=track.id) }}" method="POST"> + <h5 class="card-header"> + {{ _("page.details.comments.new.title") }} + </h5> + <div class="card-body"> + <input type="text" class="form-control mb-3" name="title" placeholder="{{ _('page.details.comments.new.input_title') }}"> + <textarea name="comment" class="form-control" rows="5" placeholder="{{ _('page.details.comments.new.input_comment') }}"></textarea> + </div> + <div class="card-footer text-muted"> + <button type="submit" class="btn btn-success"><i class="bi bi-pen"></i> {{ _("page.details.comments.new.submit") }}</button> + </div> + </form> + </div> + {% endif %} </div> {% endblock %} diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index 19d2a80..f8fc54b 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -1,4 +1,6 @@ """Track detail views.""" +import datetime + from pyramid.view import view_config from pyramid.response import Response from pyramid.httpexceptions import HTTPForbidden, HTTPFound @@ -7,6 +9,7 @@ from sqlalchemy import select from .. import models, util + @view_config(route_name='details', renderer='fietsboek:templates/details.jinja2') def details(request): """Renders the detail page for a given track. @@ -27,6 +30,7 @@ def details(request): 'track': track, 'show_edit_link': show_edit_link, 'mps_to_kph': util.mps_to_kph, + 'comment_md_to_html': util.safe_markdown, 'description': description, } @@ -76,3 +80,29 @@ def badge(request): query = select(models.Badge).filter_by(id=request.matchdict["id"]) badge_object = request.dbsession.execute(query).scalar_one() return Response(badge_object.image) + + +@view_config(route_name="add-comment", request_method="POST", permission="comment") +def add_comment(request): + """Endpoint to add a comment to a track. + + :param request: The Pyramid request. + :type request: pyramid.request.Request + :return: The HTTP response. + :rtype: pyramid.response.Response + """ + query = select(models.Track).filter_by(id=request.matchdict["track_id"]) + track = request.dbsession.execute(query).scalar_one() + if (not track.is_visible_to(request.identity) + and request.GET.get('secret') != track.link_secret): + return HTTPForbidden() + + comment = models.Comment( + track=track, + author=request.identity, + date=datetime.datetime.now(), + title=request.params["title"], + text=request.params["comment"], + ) + request.dbsession.add(comment) + return HTTPFound(request.route_url("details", id=track.id)) |