aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/models/__init__.py1
-rw-r--r--fietsboek/models/comment.py44
-rw-r--r--fietsboek/models/track.py4
-rw-r--r--fietsboek/models/user.py3
-rw-r--r--fietsboek/routes.py1
-rw-r--r--fietsboek/templates/details.jinja232
-rw-r--r--fietsboek/views/detail.py30
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))